Repository: binref/refinery (language: python)

Issue:
machometa
### Specification
Unit Request for machometa 

Requesting a new unit to perform similar functionality as pemeta, but for macho executables. The data requested inside of this would ideally include:
 - Header info (machine arch, num of load commands, etc  -  this is already parsed in peek)
 
 - Dylibs (externally linked dependencies / libs - lief can parse this )
 
 - Code-signing information (mostly just the name but if extracting other information is readily do-able, that would be great! Example parsing in Go: https://github.com/blacktop/go-macho/blob/master/pkg/codesign/codesign.go ) 
 
 - Imports/Exports/Symbols (optional flag - lief can parse this information)
 
 - Class Information (Not necessary but cool as an option flag - ObjC files only) (via class-dump - not python :( https://github.com/nygard/class-dump ))


I realize I'm not giving a ton of great info here but I'm currently using a bunch of Apple utilities or python snippets to triage Macho samples and I figured others would benefit from this being in binref :) 

### Example (awful) Parsing Code to show dylibs and signature data - works for most ARM and x86 bins but not FATs 
```
import sys
import lief
from hashlib import md5
import argparse
import os

def main():

	parser = argparse.ArgumentParser(description="Macho Sig Name Extraction")
	parser.add_argument("-f", "--file", help="Specify file to parse", metavar="<file>", required=False)
	args = parser.parse_args()

	result = sig_reading(args.file)
	if result is not None:
		print("\tTarget File: \t" + result[0])
		print("\tFile MD5: \t" + result[1])
		print("\tSig Name: \t" + result[2])
		print("\tDylibs: \t")
		for i in result[3]:
			print("\t\t"+i)



def sig_reading(target):

	dylibs = []

	try:
		target_macho = open(target, mode="rb")
		contents = target_macho.read()
		file_hash = md5(contents).hexdigest()
		parsed_macho = lief.parse(target)
		for lib in parsed_macho.libraries:
			dylibs.append(lib.name.lower())
		cs_sign_dir_offset = parsed_macho.code_signature.data_offset

		# read the big CS directory & get ptr to 0th blob
		target_macho.seek(cs_sign_dir_offset)
		cs_dir_bytes = target_macho.read(0x20)
		jump_to_blob = cs_dir_bytes[19]

		# read the 0th blob and look for ident ofset
		target_macho.seek(cs_sign_dir_offset+jump_to_blob)
		first_codesign_blob = target_macho.read(0x20)
		jump_to_ident = first_codesign_blob[23]

		# read identifier string
		target_macho.seek(cs_sign_dir_offset+jump_to_blob+jump_to_ident)
		ident_str = target_macho.read(0x30)
		sig_name = str(ident_str)
		sig_name = sig_name.split('\\x00')[0]
		sig_name = sig_name[2:]
		target_macho.close()
	except:
		print("not a macho")
		return


	sig_list = [target,file_hash,sig_name,dylibs]
	return sig_list
main()
``` 


Discussion / hints:
Do you happen to have a signed MachO-Binary that I can work with?
One note straight off the bat: I tried LIEF before (there's even [a branch called lief](https://github.com/binref/refinery/tree/lief)), but it had too many issues to make it into the main branch. Tragically enough, one main reason to discard it was that it failed to parse a sample Mach-O file. Furthermore, LIEF has some performance issues; for example, it is outperformed by the pure-python pefile module for signature extraction, see https://github.com/lief-project/LIEF/issues/832#issuecomment-1342320348.

I am currently still using the [macholib](https://github.com/ronaldoussoren/macholib) library to parse Mach-O files. I am reluctant to include LIEF _only_ for the machometa unit; my dependency tree is already way too bloated and I would prefer to prune it rather than extend it. If it is at all possible, I would much prefer to do this with macholib, but I am not yet sure if that's feasible.
Ad-hoc signed Macho: https://bazaar.abuse.ch/sample/6c121f2b2efa6592c2c22b29218157ec9e63f385e7a1d7425857d603ddef8c59/

Apple Authorized Signed Macho: https://bazaar.abuse.ch/sample/a64fa9f1c76457ecc58402142a8728ce34ccba378c17318b3340083eeb7acc67/

in regards to using macholib - I'll check it out! totally understand not wanting to bring lief in if its caused issues before. I'll investigate using it! 
initial look makes me thing it can't grab the signature data in a straightforward manner (https://github.com/ronaldoussoren/macholib/blob/0f77e700f84690121bba872470bc825cecd23d97/macholib/MachO.py#L326), but it will grab the LoadCommand for it, so theoretically we could manually parse it from there. 

it looks like dylibs can be extracted via  `macholib.dylib.dylib_info` in some form. 

for https://bazaar.abuse.ch/sample/6c121f2b2efa6592c2c22b29218157ec9e63f385e7a1d7425857d603ddef8c59/, the dylibs & signature identifier are: 

```
	Dylibs: 	
		/system/library/frameworks/foundation.framework/versions/c/foundation
		/usr/lib/libobjc.a.dylib
		/usr/lib/libc++.1.dylib
		/usr/lib/libsystem.b.dylib
		/system/library/frameworks/corefoundation.framework/versions/a/corefoundation


	Sig Name: 	payload2
```
Sweet. I am sure I can figure this out. Full disclosure, though: We have a 3 weeks old newborn right now and I'll need a bit longer to get this done than usual xD.
@huettenhain go have fun with the kid - those early days are precious, and thats much more important than my silly unit request <3 
Might be a nice alternative to lief! 

https://github.com/cxnder/ktool

Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index 7478ed08e7..c990d848ce 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -2,6 +2,7 @@
 requires = [
     "colorama",
     "defusedxml",
+    "k2l",
     "macholib",
     "msgpack>=1.0.0",
     "pefile",
diff --git a/refinery/units/formats/macho/__init__.py b/refinery/units/formats/macho/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/refinery/units/formats/macho/machometa.py b/refinery/units/formats/macho/machometa.py
new file mode 100644
index 0000000000..e3732e8746
--- /dev/null
+++ b/refinery/units/formats/macho/machometa.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import json
+
+from contextlib import suppress
+from io import BytesIO
+from typing import Dict, List
+
+from ktool import load_image, load_macho_file, Image, MachOFileType
+from ktool.macho import build_version_command, LOAD_COMMAND, source_version_command, Struct, uint8_t, uint32_t
+from ktool.codesign import Blob, BlobIndex, CSSLOT_CODEDIRECTORY, SuperBlob, swap_32
+
+from refinery.units import Arg, Unit
+from refinery.units.sinks.ppjson import ppjson
+
+
+class CodeDirectoryBlob(Struct):
+    _FIELDNAMES = [
+        "magic",
+        "length",
+        "version",
+        "flags",
+        "hashOffset",
+        "identOffset",
+        "nSpecialSlots",
+        "nCodeSlots",
+        "codeLimit",
+        "hashSize",
+        "hashType",
+        "platform",
+        "pageSize",
+        "spare2"
+    ]
+    _SIZES = [
+        uint32_t,
+        uint32_t,
+        uint32_t,
+        uint32_t,
+        uint32_t,
+        uint32_t,
+        uint32_t,
+        uint32_t,
+        uint32_t,
+        uint8_t,
+        uint8_t,
+        uint8_t,
+        uint8_t,
+        uint32_t
+    ]
+    SIZE = 44
+
+    def __init__(self, byte_order="little"):
+        super().__init__(fields=self._FIELDNAMES, sizes=self._SIZES, byte_order=byte_order)
+        self.magic = 0
+        self.length = 0
+        self.version = 0
+        self.flags = 0
+        self.hashOffset = 0
+        self.identOffset = 0
+        self.nSpecialSlots = 0
+        self.nCodeSlots = 0
+        self.codeLimit = 0
+        self.hashSize = 0
+        self.hashType = 0
+        self.platform = 0
+        self.pageSize = 0
+        self.spare2 = 0
+
+
+CS_ADHOC = 0x0000_0002
+
+
+class machometa(Unit):
+    """
+    Extract metadata from Mach-O files.
+    """
+    def __init__(
+        self, all: Arg('-c', '--custom',
+            help='Unless enabled, all default categories will be extracted.') = True,
+        header: Arg('-H', help='Parse basic data from the Mach-O header.') = False,
+        linked_images: Arg('-K', help='Parse all library images linked by the Mach-O.') = False,
+        signatures: Arg('-S', help='Parse signature and entitlement information.') = False,
+        version: Arg('-V', help="Parse version information from the Mach-O load commands.") = False,
+        load_commands: Arg('-D', help='Parse load commands from the Mach-O header.') = False,
+        exports: Arg('-E', help='List all exported functions.') = False,
+        imports: Arg('-I', help='List all imported functions.') = False,
+        tabular: Arg('-t', help='Print information in a table rather than as JSON') = False,
+    ):
+        super().__init__(
+            header=all or header,
+            linked_images=all or linked_images,
+            version=all or version,
+            signatures=all or signatures,
+            load_commands=load_commands,
+            imports=imports,
+            exports=exports,
+            tabular=tabular,
+        )
+
+    @classmethod
+    def parse_pkcs7_signature(cls, data: bytearray) -> dict:
+        """
+        Extracts a JSON-serializable and human readable dictionary with information about
+        time stamp and code signing certificates.
+        Shamelessly copied directly from the pemeta unit.
+        """
+        from refinery.units.formats.pkcs7 import pkcs7
+
+        try:
+            signature = data | pkcs7 | json.loads
+        except Exception as E:
+            raise ValueError(F'PKCS7 parser failed with error: {E!s}')
+
+        info = {}
+
+        def find_timestamps(entry):
+            if isinstance(entry, dict):
+                if set(entry.keys()) == {'type', 'value'}:
+                    if entry['type'] == 'signing_time':
+                        return {'Timestamp': entry['value']}
+                for value in entry.values():
+                    result = find_timestamps(value)
+                    if result is None:
+                        continue
+                    with suppress(KeyError):
+                        result.setdefault('TimestampIssuer', entry['sid']['issuer']['common_name'])
+                    return result
+            elif isinstance(entry, list):
+                for value in entry:
+                    result = find_timestamps(value)
+                    if result is None:
+                        continue
+                    return result
+
+        timestamp_info = find_timestamps(signature)
+        if timestamp_info is not None:
+            info.update(timestamp_info)
+
+        try:
+            certificates = signature['content']['certificates']
+        except KeyError:
+            return info
+
+        if len(certificates) == 1:
+            main_certificate = certificates[0]
+        else:
+            certificates_with_extended_use = []
+            main_certificate = None
+            for certificate in certificates:
+                with suppress(Exception):
+                    crt = certificate['tbs_certificate']
+                    ext = [e for e in crt['extensions'] if e['extn_id'] == 'extended_key_usage' and e['extn_value'] != ['time_stamping']]
+                    key = [e for e in crt['extensions'] if e['extn_id'] == 'key_usage']
+                    if ext:
+                        certificates_with_extended_use.append(certificate)
+                    if any('key_cert_sign' in e['extn_value'] for e in key):
+                        continue
+                    if any('code_signing' in e['extn_value'] for e in ext):
+                        main_certificate = certificate
+                        break
+            if main_certificate is None and len(certificates_with_extended_use) == 1:
+                main_certificate = certificates_with_extended_use[0]
+        if main_certificate:
+            crt = main_certificate['tbs_certificate']
+            serial = crt['serial_number']
+            if isinstance(serial, int):
+                serial = F'{serial:x}'
+            assert bytes.fromhex(serial) in data
+            subject = crt['subject']
+            location = [subject.get(t, '') for t in ('locality_name', 'state_or_province_name', 'country_name')]
+            info.update(Subject=subject['common_name'])
+            if any(location):
+                info.update(SubjectLocation=', '.join(filter(None, location)))
+            for signer_info in signature['content'].get('signer_infos', ()):
+                try:
+                    if signer_info['sid']['serial_number'] != crt['serial_number']:
+                        continue
+                    for attr in signer_info['signed_attrs']:
+                        if attr['type'] == 'authenticode_info':
+                            info.update(ProgramName=attr['value']['programName'])
+                            info.update(MoreInfo=attr['value']['moreInfo'])
+                except KeyError:
+                    continue
+            try:
+                valid_from = crt['validity']['not_before']
+                valid_until = crt['validity']['not_after']
+            except KeyError:
+                pass
+            else:
+                info.update(ValidFrom=valid_from, ValidUntil=valid_until)
+            info.update(
+                Issuer=crt['issuer']['common_name'], Fingerprint=main_certificate['fingerprint'], Serial=serial)
+            return info
+        return info
+
+    def parse_macho_header(self, macho_image: Image, data=None) -> Dict:
+        info = {}
+        macho_header = macho_image.macho_header
+        dyld_header = macho_image.macho_header.dyld_header
+        if dyld_header is not None:
+            info['type'] = dyld_header.typename()
+            info['magic'] = dyld_header.magic
+            info['cputype'] = macho_image.slice.type.name
+            info['cpusubtype'] = macho_image.slice.subtype.name
+            info['filetype'] = macho_image.macho_header.filetype.name
+            info['loadcount'] = dyld_header.loadcnt
+            info['loadsize'] = dyld_header.loadsize
+            info['flags'] = [flag.name for flag in macho_header.flags]
+            info['reserved'] = dyld_header.reserved
+        return info
+
+    def parse_linked_images(self, macho_image: Image, data=None) -> Dict:
+        load_command_images = {}
+        linked_images = macho_image.linked_images
+        for linked_image in linked_images:
+            load_command_name = LOAD_COMMAND(linked_image.cmd.cmd).name
+            load_command_images.setdefault(load_command_name, []).append(linked_image.install_name)
+        return load_command_images
+
+    def parse_signature(self, macho_image: Image, data=None) -> Dict:
+        info = {}
+        if macho_image.codesign_info is not None:
+            superblob: SuperBlob = macho_image.codesign_info.superblob
+
+            for blob in macho_image.codesign_info.slots:
+                blob: BlobIndex
+                # ktool does not include code for extracting Blobs of types
+                # CSSLOT_CODEDIRECTORY, CSSLOT_CMS_SIGNATURE
+                # so we must do it ourselves here.
+                if blob.type == CSSLOT_CODEDIRECTORY:
+                    start = superblob.off + blob.offset
+                    codedirectory_blob = macho_image.load_struct(start, CodeDirectoryBlob)
+
+                    # Ad-hoc signing
+                    flags = swap_32(codedirectory_blob.flags)
+                    if flags & CS_ADHOC != 0:
+                        info['Ad-Hoc Signed'] = True
+                    else:
+                        info['Ad-Hoc Signed'] = False
+
+                    # Signature identifier
+                    identifier_offset = swap_32(codedirectory_blob.identOffset)
+                    identifier_data = macho_image.get_cstr_at(start + identifier_offset)
+                    info['Signature Identifier'] = identifier_data
+
+                if blob.type == 0x10000:  # CSSLOT_CMS_SIGNATURE
+                    start = superblob.off + blob.offset
+                    blob_data = macho_image.load_struct(start, Blob)
+                    blob_data.magic = swap_32(blob_data.magic)
+                    blob_data.length = swap_32(blob_data.length)
+                    cms_signature = macho_image.get_bytes_at(start + Blob.SIZE, blob_data.length - Blob.SIZE)
+
+                    if len(cms_signature) != 0:
+                        try:
+                            parsed_cms_signature = self.parse_pkcs7_signature(bytearray(cms_signature))
+                            info['Signature'] = parsed_cms_signature
+                        except ValueError as pkcs7_parse_error:
+                            self.log_warn(f"Could not parse the data in CSSLOT_CMS_SIGNATURE as valid PKCS7 data: {pkcs7_parse_error!s}")
+
+            if macho_image.codesign_info.req_dat is not None:
+                # TODO: Parse the requirements blob,
+                # which is encoded according to the code signing requirements language:
+                # https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html
+                info['Requirements'] = macho_image.codesign_info.req_dat.hex()
+            if macho_image.codesign_info.entitlements is not None:
+                info['Entitlements'] = macho_image.codesign_info.entitlements
+        return info
+
+    def parse_version(self, macho_image: Image, data=None) -> Dict:
+        info = {}
+        load_commands = macho_image.macho_header.load_commands
+        for load_command in load_commands:
+            if isinstance(load_command, source_version_command):
+                if "SourceVersion" not in info:
+                    info["SourceVersion"] = load_command.version
+                else:
+                    self.log_warn("More than one load command of type source_version_command found; the MachO file is possibly malformed")
+            elif isinstance(load_command, build_version_command):
+                if "BuildVersion" not in info:
+                    info["BuildVersion"] = {}
+                    info["BuildVersion"]["Platform"] = macho_image.platform.name
+                    info["BuildVersion"]["MinOS"] = f"{macho_image.minos.x}.{macho_image.minos.y}.{macho_image.minos.z}"
+                    info["BuildVersion"]["SDK"] = f"{macho_image.sdk_version.x}.{macho_image.sdk_version.y}.{macho_image.sdk_version.z}"
+                    info["BuildVersion"]["Ntools"] = load_command.ntools
+                else:
+                    self.log_warn("More than one load command of type build_version_command found; the MachO file is possibly malformed")
+        return info
+
+    def parse_load_commands(self, macho_image: Image, data=None) -> List:
+        info = []
+        load_commands = macho_image.macho_header.load_commands
+        for load_command in load_commands:
+            info.append(load_command.serialize())
+        return info
+
+    def parse_imports(self, macho_image: Image, data=None) -> List:
+        info = []
+        for imp in macho_image.imports:
+            info.append(imp.name)
+        return info
+
+    def parse_exports(self, macho_image: Image, data=None) -> List:
+        info = []
+        for exp in macho_image.exports:
+            info.append(exp.name)
+        return info
+
+    def process(self, data: bytearray):
+        result = {}
+        macho = load_macho_file(fp=BytesIO(data), use_mmaped_io=False)
+        if macho.type is MachOFileType.FAT:
+            result['FileType'] = 'FAT'
+        elif macho.type is MachOFileType.THIN:
+            result['FileType'] = 'THIN'
+
+        result['Slices'] = []
+
+        for macho_slice in macho.slices:
+            slice_result = {}
+            macho_image = load_image(fp=macho_slice)
+
+            for switch, resolver, name in [
+                (self.args.header, self.parse_macho_header, 'Header'),
+                (self.args.linked_images, self.parse_linked_images, 'Linked Images'),
+                (self.args.signatures, self.parse_signature, 'Signatures'),
+                (self.args.version, self.parse_version, 'Version'),
+                (self.args.load_commands, self.parse_load_commands, 'Load Commands'),
+                (self.args.imports, self.parse_imports, 'Imports'),
+                (self.args.exports, self.parse_exports, 'Exports'),
+            ]:
+                if not switch:
+                    continue
+                self.log_debug(F'parsing: {name}')
+                try:
+                    info = resolver(macho_image, data)
+                except Exception as E:
+                    self.log_info(F'failed to obtain {name}: {E!s}')
+                    continue
+                if info:
+                    slice_result[name] = info
+
+            if macho_image.uuid is not None:
+                uuid: bytes = macho_image.uuid
+                slice_result['UUID'] = uuid.hex()
+            slice_result['Base Name'] = macho_image.base_name
+            slice_result['Install Name'] = macho_image.install_name
+
+            result['Slices'].append(slice_result)
+
+        if result:
+            yield from ppjson(tabular=self.args.tabular)._pretty_output(result, indent=4, ensure_ascii=False)


Test patch:
diff --git a/test/units/formats/macho/__init__.py b/test/units/formats/macho/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/units/formats/macho/test_machometa.py b/test/units/formats/macho/test_machometa.py
new file mode 100644
index 0000000000..057cdd100a
--- /dev/null
+++ b/test/units/formats/macho/test_machometa.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import json
+from ... import TestUnitBase
+
+
+class TestMachoMeta(TestUnitBase):
+    def test_x86_64_sample(self):
+        data = self.download_sample('6c121f2b2efa6592c2c22b29218157ec9e63f385e7a1d7425857d603ddef8c59')
+        unit = self.load()
+        result = json.loads(unit(data))
+
+        self.assertEqual(result['FileType'], "THIN")
+        self.assertEqual(len(result['Slices']), 1)
+
+        slice_metadata = result['Slices'][0]
+        self.assertIn('Header', slice_metadata)
+        self.assertIn('Linked Images', slice_metadata)
+        self.assertIn('Signatures', slice_metadata)
+        self.assertIn('Version', slice_metadata)
+
+        self.assertEqual(slice_metadata['Header']['magic'], 0xFEED_FACF)
+        self.assertEqual(slice_metadata['Header']['cputype'], "X86_64")
+        self.assertEqual(slice_metadata['Header']['cpusubtype'], "ALL")
+        self.assertEqual(slice_metadata['Header']['filetype'], "EXECUTE")
+        self.assertEqual(slice_metadata['Header']['loadcount'], 0x15)
+        self.assertEqual(slice_metadata['Header']['loadsize'], 0x8C8)
+        self.assertListEqual(slice_metadata['Header']['flags'], ["NOUNDEFS", "DYLDLINK", "TWOLEVEL", "PIE"])
+        self.assertEqual(slice_metadata['Header']['reserved'], 0)
+
+        self.assertIn('BuildVersion', slice_metadata['Version'])
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['Platform'], 'MACOS')
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['MinOS'], '10.15.0')
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['SDK'], '10.15.0')
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['Ntools'], 1)
+        self.assertEqual(slice_metadata['Version']['SourceVersion'], 0)
+
+        self.assertEqual(slice_metadata["Base Name"], "")
+        self.assertEqual(slice_metadata["Install Name"], "")
+        self.assertEqual(slice_metadata["UUID"], "839216049d683075bc3f5a8628778bb8")
+
+    def test_arm64_sample(self):
+        data = self.download_sample('3e4bbd21756ae30c24ff7d6942656be024139f8180b7bddd4e5c62a9dfbd8c79')
+        unit = self.load()
+        result = json.loads(unit(data))
+
+        self.assertEqual(result['FileType'], "THIN")
+        self.assertEqual(len(result['Slices']), 1)
+
+        slice_metadata = result['Slices'][0]
+        self.assertIn('Header', slice_metadata)
+        self.assertIn('Linked Images', slice_metadata)
+        self.assertIn('Signatures', slice_metadata)
+        self.assertIn('Version', slice_metadata)
+
+        self.assertEqual(slice_metadata['Header']['magic'], 0xFEED_FACF)
+        self.assertEqual(slice_metadata['Header']['cputype'], "ARM64")
+        self.assertEqual(slice_metadata['Header']['cpusubtype'], "ALL")
+        self.assertEqual(slice_metadata['Header']['filetype'], "EXECUTE")
+        self.assertEqual(slice_metadata['Header']['loadcount'], 0x11)
+        self.assertEqual(slice_metadata['Header']['loadsize'], 0x6F8)
+        self.assertListEqual(slice_metadata['Header']['flags'], ["NOUNDEFS", "DYLDLINK", "TWOLEVEL", "PIE"])
+        self.assertEqual(slice_metadata['Header']['reserved'], 0)
+
+        self.assertIn('BuildVersion', slice_metadata['Version'])
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['Platform'], 'MACOS')
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['MinOS'], '11.0.0')
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['SDK'], '11.3.0')
+        self.assertEqual(slice_metadata['Version']['BuildVersion']['Ntools'], 1)
+        self.assertEqual(slice_metadata['Version']['SourceVersion'], 0)
+
+        self.assertEqual(slice_metadata["Base Name"], "")
+        self.assertEqual(slice_metadata["Install Name"], "")
+        self.assertEqual(slice_metadata["UUID"], "f962f18b12a133368aa40779089c2b09")
+
+    def test_universal_binary_sample(self):
+        data = self.download_sample('1a9a5c797777f37463b44de2b49a7f95abca786db3977dcdac0f79da739c08ac')
+        unit = self.load()
+        result = json.loads(unit(data))
+
+        self.assertEqual(result['FileType'], "FAT")
+        self.assertEqual(len(result['Slices']), 2)
+
+        for slice_metadata in result['Slices']:
+            self.assertIn('Header', slice_metadata)
+            self.assertIn('Linked Images', slice_metadata)
+            self.assertIn('Signatures', slice_metadata)
+            self.assertIn('Version', slice_metadata)
+
+            self.assertIn('BuildVersion', slice_metadata['Version'])
+            self.assertEqual(slice_metadata['Version']['BuildVersion']['Platform'], 'MACOS')
+            self.assertEqual(slice_metadata['Version']['BuildVersion']['MinOS'], '11.3.0')
+            self.assertEqual(slice_metadata['Version']['BuildVersion']['SDK'], '11.3.0')
+            self.assertEqual(slice_metadata['Version']['BuildVersion']['Ntools'], 1)
+            self.assertEqual(slice_metadata['Version']['SourceVersion'], 0)
+
+        x86_64_slice_metadata = result['Slices'][0]
+        self.assertEqual(x86_64_slice_metadata['Header']['type'], "mach_header_64")
+        self.assertEqual(x86_64_slice_metadata['Header']['magic'], 0xFEED_FACF)
+        self.assertEqual(x86_64_slice_metadata['Header']['cputype'], "X86_64")
+        self.assertEqual(x86_64_slice_metadata['Header']['cpusubtype'], "ALL")
+        self.assertEqual(x86_64_slice_metadata['Header']['filetype'], "EXECUTE")
+        self.assertEqual(x86_64_slice_metadata['Header']['loadcount'], 0x13)
+        self.assertEqual(x86_64_slice_metadata['Header']['loadsize'], 0x760)
+        self.assertListEqual(x86_64_slice_metadata['Header']['flags'],
+            ["NOUNDEFS", "DYLDLINK", "TWOLEVEL", "WEAK_DEFINES", "BINDS_TO_WEAK", "PIE"])
+        self.assertEqual(x86_64_slice_metadata['Header']['reserved'], 0)
+        self.assertEqual(x86_64_slice_metadata['UUID'], '8174817ef4cf398d975b7860466eaec7')
+
+        arm64_slice_metadata = result['Slices'][1]
+        self.assertEqual(arm64_slice_metadata['Header']['magic'], 0xFEED_FACF)
+        self.assertEqual(arm64_slice_metadata['Header']['cputype'], "ARM64")
+        self.assertEqual(arm64_slice_metadata['Header']['cpusubtype'], "ALL")
+        self.assertEqual(arm64_slice_metadata['Header']['filetype'], "EXECUTE")
+        self.assertEqual(arm64_slice_metadata['Header']['loadcount'], 0x13)
+        self.assertEqual(arm64_slice_metadata['Header']['loadsize'], 0x7B0)
+        self.assertListEqual(arm64_slice_metadata['Header']['flags'],
+            ["NOUNDEFS", "DYLDLINK", "TWOLEVEL", "WEAK_DEFINES", "BINDS_TO_WEAK", "PIE"])
+        self.assertEqual(arm64_slice_metadata['Header']['reserved'], 0)
+        self.assertEqual(arm64_slice_metadata['UUID'], 'ec10d84e723f3d9a8524cdc706749d68')
+
+    def test_adhoc_signature(self):
+        data = self.download_sample('6c121f2b2efa6592c2c22b29218157ec9e63f385e7a1d7425857d603ddef8c59')
+        unit = self.load()
+        result = json.loads(unit(data))
+
+        self.assertEqual(result['FileType'], "THIN")
+        self.assertEqual(len(result['Slices']), 1)
+
+        slice_metadata = result['Slices'][0]
+        self.assertIn('Signatures', slice_metadata)
+        self.assertEqual(slice_metadata['Signatures']['Ad-Hoc Signed'], True)
+        self.assertEqual(slice_metadata['Signatures']['Signature Identifier'], "payload2-55554944839216049d683075bc3f5a8628778bb8")
+
+    def test_pkcs7_signature(self):
+        data = self.download_sample('a64fa9f1c76457ecc58402142a8728ce34ccba378c17318b3340083eeb7acc67')
+        unit = self.load()
+        result = json.loads(unit(data))
+
+        self.assertEqual(result['FileType'], "FAT")
+        self.assertEqual(len(result['Slices']), 2)
+
+        for slice_index, slice_metadata in enumerate(result['Slices']):
+            self.assertIn('Signatures', slice_metadata)
+            self.assertIn('Ad-Hoc Signed', slice_metadata['Signatures'])
+            self.assertIn('Signature Identifier', slice_metadata['Signatures'])
+            self.assertIn('Signature', slice_metadata['Signatures'])
+
+            self.assertEqual(slice_metadata['Signatures']['Signature Identifier'], "libffmpeg")
+            self.assertEqual(slice_metadata['Signatures']['Ad-Hoc Signed'], False)
+
+            pkcs7_signature_data = slice_metadata['Signatures']['Signature']
+
+            if slice_index == 0:
+                self.assertEqual(pkcs7_signature_data['Timestamp'], "2023-03-13 06:41:00+00:00")
+            elif slice_index == 1:
+                self.assertEqual(pkcs7_signature_data['Timestamp'], "2023-03-13 06:41:01+00:00")
+
+            self.assertEqual(pkcs7_signature_data['TimestampIssuer'], "Developer ID Certification Authority")
+            self.assertEqual(pkcs7_signature_data['Subject'], "Developer ID Application: 3CX (33CF4654HL)")
+            self.assertEqual(pkcs7_signature_data['SubjectLocation'], "US")
+            self.assertEqual(pkcs7_signature_data['ValidFrom'], "2019-04-11 12:03:36+00:00")
+            self.assertEqual(pkcs7_signature_data['ValidUntil'], "2024-04-11 12:03:36+00:00")
+            self.assertEqual(pkcs7_signature_data['Issuer'], "Developer ID Certification Authority")
+            self.assertEqual(pkcs7_signature_data['Fingerprint'], "7df5ed6d71b296ed073a5b3efbcdc4c916ba41be")
+            self.assertEqual(pkcs7_signature_data['Serial'], "4b0aaf622b260469")
+
+    def test_linked_images(self):
+        data = self.download_sample('38c9b858c32fcc6b484272a182ae6e7f911dea53a486396037d8f7956d2110be')
+        unit = self.load()
+        result = json.loads(unit(data))
+
+        self.assertEqual(result['FileType'], "FAT")
+        self.assertEqual(len(result['Slices']), 2)
+
+        for slice_metadata in result['Slices']:
+            self.assertIn('Linked Images', slice_metadata)
+            expected_load_dylibs = {
+                "LOAD_WEAK_DYLIB": [
+                    "/usr/lib/swift/libswiftAppKit.dylib",
+                    "/usr/lib/swift/libswiftCloudKit.dylib",
+                    "/usr/lib/swift/libswiftCoreData.dylib",
+                    "/usr/lib/swift/libswiftCoreFoundation.dylib",
+                    "/usr/lib/swift/libswiftCoreGraphics.dylib",
+                    "/usr/lib/swift/libswiftCoreImage.dylib",
+                    "/usr/lib/swift/libswiftCoreLocation.dylib",
+                    "/usr/lib/swift/libswiftDarwin.dylib",
+                    "/usr/lib/swift/libswiftDispatch.dylib",
+                    "/usr/lib/swift/libswiftIOKit.dylib",
+                    "/usr/lib/swift/libswiftMetal.dylib",
+                    "/usr/lib/swift/libswiftObjectiveC.dylib",
+                    "/usr/lib/swift/libswiftQuartzCore.dylib",
+                    "/usr/lib/swift/libswiftUniformTypeIdentifiers.dylib",
+                    "/usr/lib/swift/libswiftXPC.dylib"
+                ],
+                "LOAD_DYLIB": [
+                    "/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation",
+                    "/usr/lib/libobjc.A.dylib",
+                    "/usr/lib/libSystem.B.dylib",
+                    "/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit",
+                    "/System/Library/Frameworks/SwiftUI.framework/Versions/A/SwiftUI",
+                    "/usr/lib/swift/libswiftCore.dylib",
+                    "/usr/lib/swift/libswiftFoundation.dylib",
+                    "/usr/lib/swift/libswiftos.dylib"
+                ],
+            }
+            self.assertDictEqual(slice_metadata['Linked Images'], expected_load_dylibs)
\ No newline at end of file