#!/usr/bin/env ruby
# Deactivates any embedded code signatures in a Mach-O binary.

module MachO
  class Unsign
    def self.unsign(filename)
      File.open(filename, "r+") do |f|
        Unsign.new(f).headers
      end
    end

    attr_accessor :headers

    protected

    FatHeader = Struct.new(:cpu_type, :cpu_subtype, :offset, :size, :align, :mach)
    MachHeader = Struct.new(:cpu_type, :cpu_subtype, :filetype, :ncmds, :sizeofcmds, :flags, :reserved, :cmds)
    LoadCommand = Struct.new(:cmd, :cmdsize)

    def initialize(f)
      @f = f
      @headers = process
    end

    def debug(message)
      puts message if ENV["DEBUG"]
    end

    def word_type
      @big_endian ? 'N' : 'V'
    end

    def patch_code_signature(lc)
      # just change LC_CODE_SIGNATURE to a high value that will be ignored by the loader
      debug "PATCHING LC_CODE_SIGNATURE"
      @f.seek(-8, IO::SEEK_CUR)
      @f.write([0xff, lc.cmdsize].pack("#{word_type}2"))
      lc
    end

    def process_mach
      len = @x86_64 ? 7 : 6
      header = MachHeader.new(*@f.read(len*4).unpack("#{word_type}#{len}"))
      debug "MACH HEADER: #{header.inspect}"
      header.cmds = (1..(header.ncmds)).collect do
        lc = LoadCommand.new(*@f.read(8).unpack("#{word_type}2"))
        debug "LOAD COMMAND: #{lc.inspect}"

        lc = case lc.cmd
        when 0x1d then patch_code_signature(lc)
        else lc
        end

        @f.seek(lc.cmdsize - 8, IO::SEEK_CUR)

        lc
      end
      header
    end

    def process_fat
      num_arches, = @f.read(4).unpack("N")
      arches = (1..num_arches).collect do
        FatHeader.new(*@f.read(20).unpack("N5"))
      end
      debug "FAT HEADER: #{arches.inspect}"
      arches.each do |arch|
        @f.seek(arch.offset)
        arch.mach = process
      end
      arches
    end

    def process
      magic, = @f.read(4).unpack("N")
      debug "MAGIC: 0x%08x" % magic
      case magic
      when 0xcafebabe then @big_endian, @x86_64 = false, false; process_fat
      when 0xfeedface then @big_endian, @x86_64 = true,  false; process_mach
      when 0xcffaedfe then @big_endian, @x86_64 = false, true;  process_mach
      when 0xcefaedfe then @big_endian, @x86_64 = false, false; process_mach
      else raise "unknown magic: 0x%08x" % magic
      end
    end
  end
end

# command line driver
if __FILE__ == $0
  if ARGV.empty?
    $stderr.puts "usage:  #{$0} filename ..."
    exit 1
  end

  ARGV.each do |filename|
    puts "removing signatures from: #{filename}"
    MachO::Unsign::unsign(filename)
  end
end