diff --git a/lib/macho.rb b/lib/macho.rb index 351b2f5de..8673ce1bd 100644 --- a/lib/macho.rb +++ b/lib/macho.rb @@ -6,6 +6,7 @@ require_relative "macho/structure" require_relative "macho/view" require_relative "macho/headers" +require_relative "macho/code_signing" require_relative "macho/load_commands" require_relative "macho/sections" require_relative "macho/macho_file" diff --git a/lib/macho/code_signing.rb b/lib/macho/code_signing.rb new file mode 100644 index 000000000..e397d9669 --- /dev/null +++ b/lib/macho/code_signing.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +module MachO + module CodeSigning + CS_STRUCTURES = { + Headers::CSMAGIC_REQUIREMENT => "Requirement", + Headers::CSMAGIC_REQUIREMENTS => "Requirements", + Headers::CSMAGIC_CODEDIRECTORY => "CodeDirectory", + Headers::CSMAGIC_EMBEDDED_SIGNATURE => "EmbeddedSignature", + Headers::CSMAGIC_EMBEDDED_SIGNATURE_OLD => "OldEmbeddedSignature", + Headers::CSMAGIC_DETACHED_SIGNATURE => "DetachedSignature", + Headers::CSMAGIC_ENTITLEMENT => "Entitlement", + Headers::CSMAGIC_ENTITLEMENTDER => "DEREntitlement", + Headers::CSMAGIC_BLOBWRAPPER => "BlobWrapper", + }.freeze + + CS_HASHTYPES = { + 1 => :CS_HASHTYPE_SHA1, + 2 => :CS_HASHTYPE_SHA256, + 3 => :CS_HASHTYPE_SHA256_TRUNCATED, + 4 => :CS_HASHTYPE_SHA384, + }.freeze + + class CSStructure < MachOStructure + attr_reader :view + + def self.new_from_bin(view) + bin = view.raw_data.slice(view.offset, bytesize) + format = Utils.specialize_format(self::FORMAT, view.endianness) + + new(view, *bin.unpack(format)) + end + + def initialize(view) + @view = view + end + + def to_h + { + "view" => view.to_h, + }.merge super + end + end + + class CSBlob < CSStructure + field :magic, :uint32, endian: :big + field :length, :uint32, endian: :big + + def magic_sym + Headers::CS_MAGICS[magic] + end + + def to_h + { + "blob" => { + "magic" => magic, + "magic_sym" => magic_sym, + "length" => length, + }, + }.merge super + end + end + + class SuperBlob < CSBlob + class BlobIndex < CSStructure + field :type, :uint32, endian: :big + field :offset, :uint32, endian: :big + end + + field :count, :uint32, endian: :big + + def self.new_from_bin(view) + bin = view.raw_data.slice(view.offset, bytesize) + format = Utils.specialize_format(self::FORMAT, view.endianness) + + new(view, *bin.unpack(format)) + end + + def initialize(view, magic, length, count) + # TODO(ww): Check magic matches CSMAGIC_EMBEDDED_SIGNATURE (0xfade0cc0) + super(view, magic, length) + @count = count + end + + def each_blob_index + count.times do |i| + index_offset = view.offset + self.class.bytesize + (BlobIndex.bytesize * i) + blob_view = MachOView.new view.raw_data, view.endianness, index_offset + yield BlobIndex.new_from_bin blob_view + end + end + + def each_blob + each_blob_index do |blob_index| + blob_offset = view.offset + blob_index.offset + blob_magic = view.raw_data[blob_offset, blob_offset + 4].unpack1("L>1") + + blob_klass_str = CS_STRUCTURES[blob_magic] + raise CSBlobUnknownError, blob_magic unless blob_klass_str + + blob_klass = CodeSigning.const_get blob_klass_str + blob_view = MachOView.new view.raw_data, view.endianness, blob_offset + yield blob_klass.new_from_bin blob_view + end + end + + def to_h + { + magic_sym.to_h => { + "count" => count, + }, + }.merge super + end + end + + # Represents a code signing Requirement blob. + class Requirement < CSBlob + end + + # Represents a code signing Requirements vector. + class Requirements < CSBlob + end + + # Represents a code signing CodeDirectory blob. + class CodeDirectory < CSBlob + attr_reader :version + attr_reader :flags + attr_reader :hash_offset + attr_reader :ident_offset + attr_reader :n_special_slots + attr_reader :n_code_slots + attr_reader :code_limit + attr_reader :hash_size + attr_reader :hash_type + attr_reader :spare1 + attr_reader :page_size + attr_reader :spare2 + + FORMAT = "L>9C4L>1" + + SIZEOF = 44 + + def initialize(view, magic, length, version, flags, hash_offset, + ident_offset, n_special_slots, n_code_slots, code_limit, + hash_size, hash_type, spare1, page_size, spare2) + super(view, magic, length) + @version = version + @flags = flags + @hash_offset = hash_offset + @ident_offset = ident_offset + @n_special_slots = n_special_slots + @n_code_slots = n_code_slots + @code_limit = code_limit + @hash_size = hash_size + @hash_type = hash_type + @spare1 = spare1 + @page_size = page_size + @spare2 = spare2 + end + + def hash_type_sym + CS_HASHTYPES[hash_type] + end + + # TODO(ww): Figure out a good way to handle the fields that have been + # added with different CodeDirectory versions: + # * 0x20100: + # uint32_t scatterOffset + # char end_withScatter[0] + # * 0x20200: + # uint32_t teamOffset + # char end_withTeam[0] + # * 0x20300: + # uint32_t spare3 + # uint64_t codeLimit64 + # char end_withCodeLimit64[0] + # * 0x20400: + # uint64_t execSegBase + # uint64_t execSegLimit + # uint64_t execSegFlags + # char end_withExecSec[0] + + def to_h + { + magic_sym.to_s => { + "version" => version, + "flags" => flags, + "hash_offset" => hash_offset, + "ident_offset" => ident_offset, + "n_special_slots" => n_special_slots, + "n_code_slots" => n_code_slots, + "code_limit" => code_limit, + "hash_size" => hash_size, + "hash_type" => hash_type, + "spare1" => spare1, + "page_size" => page_size, + "spare2" => spare2, + }, + }.merge super + end + end + + # Represents a code signing EmbeddedSignature blob. + class EmbeddedSignature < CSBlob + end + + # Maybe represents an "old" embedded signature blob. + # Not documented. + class OldEmbeddedSignature < CSBlob + end + + # Represents a multi-arch collection of embedded signatures. + class DetachedSignature < CSBlob + end + + # Represents a code signing Entitlement blob. + class Entitlement < CSBlob + # NOTE(ww): This appears to just have one member, data, whose length + # is defined by attr :length. + # From SecTask.c: + # const struct theBlob { + # uint32_t magic; /* kSecCodeMagicEntitlement */ + # uint32_t length; + # const uint8_t data[]; + # } + end + + class DEREntitlement < CSBlob + end + + class BlobWrapper < CSBlob + end + end +end diff --git a/lib/macho/exceptions.rb b/lib/macho/exceptions.rb index 66af33427..63d32d251 100644 --- a/lib/macho/exceptions.rb +++ b/lib/macho/exceptions.rb @@ -230,6 +230,22 @@ def initialize(offset) end end + # TODO(ww): doc + class LinkeditTypeMismatchError < MachOError + # TODO(ww): doc + def initialize(meth, lc_sym) + super "Method #{meth} can't be used on __LINKEDIT data commands of type #{lc_sym}" + end + end + + # TODO(ww): doc + class CSBlobUnknownError < MachOError + # TODO(ww): doc + def initialize(magic) + super "Unknown code signing blob magic: 0x#{magic.to_s 16}" + end + end + # Raised when attempting to parse a compressed Mach-O without explicitly # requesting decompression. class CompressedMachOError < MachOError diff --git a/lib/macho/headers.rb b/lib/macho/headers.rb index 4abdd533d..4df8b37b1 100644 --- a/lib/macho/headers.rb +++ b/lib/macho/headers.rb @@ -501,6 +501,32 @@ module Headers :MH_DYLIB_IN_CACHE => 0x80000000, }.freeze + # the blob index (presumably) for the CodeDirectory blob + # @api private + CSSLOT_CODEDIRECTORY = 0 + + CSMAGIC_REQUIREMENT = 0xfade0c00 + CSMAGIC_REQUIREMENTS = 0xfade0c01 + CSMAGIC_CODEDIRECTORY = 0xfade0c02 + CSMAGIC_EMBEDDED_SIGNATURE = 0xfade0cc0 + CSMAGIC_EMBEDDED_SIGNATURE_OLD = 0xfade0b02 + CSMAGIC_DETACHED_SIGNATURE = 0xfade0cc0 + CSMAGIC_ENTITLEMENT = 0xfade7171 + CSMAGIC_ENTITLEMENTDER = 0xfade7172 + CSMAGIC_BLOBWRAPPER = 0xfade0b01 + + CS_MAGICS = { + CSMAGIC_REQUIREMENT => :CSMAGIC_REQUIREMENT, + CSMAGIC_REQUIREMENTS => :CSMAGIC_REQUIREMENTS, + CSMAGIC_CODEDIRECTORY => :CSMAGIC_CODEDIRECTORY, + CSMAGIC_EMBEDDED_SIGNATURE => :CSMAGIC_EMBEDDED_SIGNATURE, + CSMAGIC_EMBEDDED_SIGNATURE_OLD => :CSMAGIC_EMBEDDED_SIGNATURE_OLD, + CSMAGIC_DETACHED_SIGNATURE => :CSMAGIC_DETACHED_SIGNATURE, + CSMAGIC_ENTITLEMENT => :CSMAGIC_ENTITLEMENT, + CSMAGIC_ENTITLEMENTDER => :CSMAGIC_ENTITLEMENTDER, + CSMAGIC_BLOBWRAPPER => :CSMAGIC_BLOBWRAPPER, + }.freeze + # Fat binary header structure # @see MachO::FatArch class FatHeader < MachOStructure diff --git a/lib/macho/load_commands.rb b/lib/macho/load_commands.rb index c420c8bd9..d9e1be2df 100644 --- a/lib/macho/load_commands.rb +++ b/lib/macho/load_commands.rb @@ -973,6 +973,14 @@ def to_h "datasize" => datasize, }.merge super end + + # TODO(ww): doc + def superblob + raise LinkeditTypeMismatchError.new(__method__, type) unless type == :LC_CODE_SIGNATURE + + superblob_view = MachOView.new view.raw_data, view.endianness, dataoff + CodeSigning::SuperBlob.new_from_bin superblob_view + end end # A load command representing the offset to and size of an encrypted