From 475ec96e0f300fd27a815fd3a258957674fea657 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 5 Oct 2020 10:53:09 -0400 Subject: [PATCH 1/5] [WIP] lib: Initial code signing work --- lib/macho.rb | 1 + lib/macho/code_signing.rb | 73 ++++++++++++++++++++++++++++++++++++++ lib/macho/exceptions.rb | 8 +++++ lib/macho/load_commands.rb | 8 +++++ 4 files changed, 90 insertions(+) create mode 100644 lib/macho/code_signing.rb diff --git a/lib/macho.rb b/lib/macho.rb index c913dff93..8b601e864 100644 --- a/lib/macho.rb +++ b/lib/macho.rb @@ -3,6 +3,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..5eaa7779f --- /dev/null +++ b/lib/macho/code_signing.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module MachO + module CodeSigning + 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 + end + + class SuperBlob < CSStructure + attr_reader :magic + + attr_reader :length + + attr_reader :count + + # NOTE(ww): SuperBlobs and other code signing structures appear to always be + # big-endian for...reasons. + FORMAT = "L>3" + + SIZEOF = 12 + + 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 = magic + @length = 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 + end + + class BlobIndex < CSStructure + attr_reader :type + + attr_reader :offset + + FORMAT = "L>2" + + SIZEOF = 8 + + def initialize(view, type, offset) + super(view) + @type = type + @offset = offset + end + end + end +end diff --git a/lib/macho/exceptions.rb b/lib/macho/exceptions.rb index 4587c3086..40263679e 100644 --- a/lib/macho/exceptions.rb +++ b/lib/macho/exceptions.rb @@ -206,4 +206,12 @@ def initialize(offset) " Consider merging with `fat64: true`" 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 end diff --git a/lib/macho/load_commands.rb b/lib/macho/load_commands.rb index e16ad5eaf..4a32a064c 100644 --- a/lib/macho/load_commands.rb +++ b/lib/macho/load_commands.rb @@ -1223,6 +1223,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 From 621a34c198cc922efd74d218bf034e468df3478f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 5 Oct 2020 20:08:22 -0400 Subject: [PATCH 2/5] lib: More code signing work --- lib/macho/code_signing.rb | 186 ++++++++++++++++++++++++++++++++++---- lib/macho/exceptions.rb | 6 ++ lib/macho/headers.rb | 26 ++++++ 3 files changed, 201 insertions(+), 17 deletions(-) diff --git a/lib/macho/code_signing.rb b/lib/macho/code_signing.rb index 5eaa7779f..646a143a5 100644 --- a/lib/macho/code_signing.rb +++ b/lib/macho/code_signing.rb @@ -2,6 +2,18 @@ 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 + class CSStructure < MachOStructure attr_reader :view @@ -15,17 +27,65 @@ def self.new_from_bin(view) def initialize(view) @view = view end + + def to_h + { + "view" => "view.to_h", + }.merge super + end end - class SuperBlob < CSStructure + class CSBlob < CSStructure attr_reader :magic attr_reader :length - attr_reader :count - # NOTE(ww): SuperBlobs and other code signing structures appear to always be # big-endian for...reasons. + FORMAT = "L>2" + + SIZEOF = 8 + + def initialize(view, magic, length) + super(view) + @magic = magic + @length = length + end + + 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 + attr_reader :type + + attr_reader :offset + + FORMAT = "L>2" + + SIZEOF = 8 + + def initialize(view, type, offset) + super(view) + @type = type + @offset = offset + end + end + + attr_reader :count + FORMAT = "L>3" SIZEOF = 12 @@ -39,35 +99,127 @@ def self.new_from_bin(view) def initialize(view, magic, length, count) # TODO(ww): Check magic matches CSMAGIC_EMBEDDED_SIGNATURE (0xfade0cc0) - super(view) - @magic = magic - @length = length + 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 + blob_view = MachOView.new view.raw_data, view.endianness, index_offset yield BlobIndex.new_from_bin blob_view end end - end - class BlobIndex < CSStructure - attr_reader :type + 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") - attr_reader :offset + blob_klass_str = CS_STRUCTURES[blob_magic] + raise CSBlobUnknownError, blob_magic unless blob_klass_str - FORMAT = "L>2" + 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 + end - SIZEOF = 8 + # Represents a code signing Requirement blob. + class Requirement < CSBlob + end - def initialize(view, type, offset) - super(view) - @type = type - @offset = offset + # 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 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 40263679e..d4dcbd84d 100644 --- a/lib/macho/exceptions.rb +++ b/lib/macho/exceptions.rb @@ -214,4 +214,10 @@ def initialize(meth, lc_sym) super "Method #{meth} can't be used on __LINKEDIT data commands of type #{lc_sym}" end end + + class CSBlobUnknownError < MachOError + def initialize(magic) + super "Unknown code signing blob magic: 0x#{magic.to_s 16}" + end + end end diff --git a/lib/macho/headers.rb b/lib/macho/headers.rb index 525809e6b..bb7a91c8d 100644 --- a/lib/macho/headers.rb +++ b/lib/macho/headers.rb @@ -480,6 +480,32 @@ module Headers :MH_APP_EXTENSION_SAFE => 0x02000000, }.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 From 8a9d313a07cae7e78515bd683074e75e0c20d83d Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 5 Oct 2020 21:28:43 -0400 Subject: [PATCH 3/5] lib: More code signing work --- lib/macho/code_signing.rb | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/lib/macho/code_signing.rb b/lib/macho/code_signing.rb index 646a143a5..8968a32be 100644 --- a/lib/macho/code_signing.rb +++ b/lib/macho/code_signing.rb @@ -14,6 +14,13 @@ module CodeSigning 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 @@ -124,6 +131,14 @@ def each_blob 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. @@ -171,6 +186,28 @@ def initialize(view, magic, length, version, flags, hash_offset, @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 => { From 2b7de7afd5d4d1f4cbca98d019fda782ae19199b Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 5 Oct 2020 21:38:02 -0400 Subject: [PATCH 4/5] lib: Typo --- lib/macho/code_signing.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/macho/code_signing.rb b/lib/macho/code_signing.rb index 8968a32be..17c316837 100644 --- a/lib/macho/code_signing.rb +++ b/lib/macho/code_signing.rb @@ -37,7 +37,7 @@ def initialize(view) def to_h { - "view" => "view.to_h", + "view" => view.to_h, }.merge super end end From a21ba7c6c6721e63354e6fd0c7d681d36efe7f77 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 9 Mar 2023 21:28:53 -0500 Subject: [PATCH 5/5] code_signing: some refactoring --- lib/macho/code_signing.rb | 38 +++++--------------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/lib/macho/code_signing.rb b/lib/macho/code_signing.rb index 17c316837..e397d9669 100644 --- a/lib/macho/code_signing.rb +++ b/lib/macho/code_signing.rb @@ -43,21 +43,8 @@ def to_h end class CSBlob < CSStructure - attr_reader :magic - - attr_reader :length - - # NOTE(ww): SuperBlobs and other code signing structures appear to always be - # big-endian for...reasons. - FORMAT = "L>2" - - SIZEOF = 8 - - def initialize(view, magic, length) - super(view) - @magic = magic - @length = length - end + field :magic, :uint32, endian: :big + field :length, :uint32, endian: :big def magic_sym Headers::CS_MAGICS[magic] @@ -76,26 +63,11 @@ def to_h class SuperBlob < CSBlob class BlobIndex < CSStructure - attr_reader :type - - attr_reader :offset - - FORMAT = "L>2" - - SIZEOF = 8 - - def initialize(view, type, offset) - super(view) - @type = type - @offset = offset - end + field :type, :uint32, endian: :big + field :offset, :uint32, endian: :big end - attr_reader :count - - FORMAT = "L>3" - - SIZEOF = 12 + field :count, :uint32, endian: :big def self.new_from_bin(view) bin = view.raw_data.slice(view.offset, bytesize)