Skip to content

Instantly share code, notes, and snippets.

@tyfkda
Last active March 26, 2025 00:02
Show Gist options
  • Save tyfkda/d61eb12fd663c574ca87387b2f31aa39 to your computer and use it in GitHub Desktop.
Save tyfkda/d61eb12fd663c574ca87387b2f31aa39 to your computer and use it in GitHub Desktop.
Mach-Oファイルを解析するツール(Ruby)
LC_REQ_DYLD = 0x80000000
LC_SEGMENT = 0x01
LC_SYMTAB = 0x02
LC_SYMSEG = 0x03
LC_THREAD = 0x04
LC_UNIXTHREAD = 0x05
LC_LOADFVMLIB = 0x06
LC_IDFVMLIB = 0x07
LC_IDENT = 0x08
LC_FVMFILE = 0x09
LC_PREPAGE = 0x0a
LC_DYSYMTAB = 0x0b
LC_LOAD_DYLIB = 0x0c
LC_ID_DYLIB = 0x0d
LC_LOAD_DYLINKER = 0x0e
LC_ID_DYLINKER = 0x0f
LC_PREBOUND_DYLIB = 0x10
LC_ROUTINES = 0x11
LC_SUB_FRAMEWORK = 0x12
LC_SUB_UMBRELLA = 0x13
LC_SUB_CLIENT = 0x14
LC_SUB_LIBRARY = 0x15
LC_TWOLEVEL_HINTS = 0x16
LC_PREBIND_CKSUM = 0x17
LC_LOAD_WEAK_DYLIB = 0x18 | LC_REQ_DYLD
LC_SEGMENT_64 = 0x19
LC_ROUTINES_64 = 0x1a
LC_UUID = 0x1b
LC_RPATH = 0x1c | LC_REQ_DYLD
LC_CODE_SIGNATURE = 0x1d
LC_SEGMENT_SPLIT_INFO =0x1e
LC_REEXPORT_DYLIB = 0x1f | LC_REQ_DYLD
LC_LAZY_LOAD_DYLIB = 0x20
LC_ENCRYPTION_INFO = 0x21
LC_DYLD_INFO = 0x22
LC_DYLD_INFO_ONLY = 0x22 | LC_REQ_DYLD
LC_LOAD_UPWARD_DYLIB = 0x23 | LC_REQ_DYLD
LC_VERSION_MIN_MACOSX = 0x24
LC_VERSION_MIN_IPHONEOS = 0x25
LC_FUNCTION_STARTS = 0x26
LC_DYLD_ENVIRONMENT = 0x27
LC_MAIN = 0x28 | LC_REQ_DYLD
LC_DATA_IN_CODE = 0x29
LC_SOURCE_VERSION = 0x2a
LC_DYLIB_CODE_SIGN_DRS = 0x2b
LC_ENCRYPTION_INFO_64 =0x2c
LC_LINKER_OPTION = 0x2d
LC_LINKER_OPTIMIZATION_HINT = 0x2e
LC_VERSION_MIN_TVOS = 0x2f
LC_VERSION_MIN_WATCHOS = 0x30
LC_NOTE = 0x31
LC_BUILD_VERSION = 0x32
LC_DYLD_EXPORTS_TRIE = 0x33 | LC_REQ_DYLD
LC_DYLD_CHAINED_FIXUPS = 0x34 | LC_REQ_DYLD
LC_FILESET_ENTRY = 0x35 | LC_REQ_DYLD
LC_ATOM_INFO = 0x36
CommandNameTable = {}
%w(
LC_SEGMENT LC_SYMTAB LC_SYMSEG LC_THREAD LC_UNIXTHREAD LC_LOADFVMLIB LC_IDFVMLIB LC_IDENT
LC_FVMFILE LC_PREPAGE LC_DYSYMTAB LC_LOAD_DYLIB LC_ID_DYLIB LC_LOAD_DYLINKER LC_ID_DYLINKER
LC_PREBOUND_DYLIB LC_ROUTINES LC_SUB_FRAMEWORK LC_SUB_UMBRELLA LC_SUB_CLIENT LC_SUB_LIBRARY
LC_TWOLEVEL_HINTS LC_PREBIND_CKSUM LC_LOAD_WEAK_DYLIB LC_SEGMENT_64 LC_ROUTINES_64 LC_UUID
LC_RPATH LC_CODE_SIGNATURE LC_SEGMENT_SPLIT_INFO LC_REEXPORT_DYLIB LC_LAZY_LOAD_DYLIB
LC_ENCRYPTION_INFO LC_DYLD_INFO LC_DYLD_INFO_ONLY LC_LOAD_UPWARD_DYLIB LC_VERSION_MIN_MACOSX
LC_VERSION_MIN_IPHONEOS LC_FUNCTION_STARTS LC_DYLD_ENVIRONMENT LC_MAIN LC_DATA_IN_CODE
LC_SOURCE_VERSION LC_DYLIB_CODE_SIGN_DRS LC_ENCRYPTION_INFO_64 LC_LINKER_OPTION
LC_LINKER_OPTIMIZATION_HINT LC_VERSION_MIN_TVOS LC_VERSION_MIN_WATCHOS LC_NOTE LC_BUILD_VERSION
LC_DYLD_EXPORTS_TRIE LC_DYLD_CHAINED_FIXUPS LC_FILESET_ENTRY LC_ATOM_INFO
).each {|s| CommandNameTable[Object.const_get(s.to_sym)] = s}
LoadCommandStructName = {
LC_SYMTAB => :symtab_command,
LC_SEGMENT_64 => :segment_command_64,
LC_DYSYMTAB => :dysymtab_command,
LC_LOAD_DYLINKER => :dylinker_command,
LC_DYLD_EXPORTS_TRIE => :linkedit_data_command,
LC_DYLD_CHAINED_FIXUPS => :linkedit_data_command,
LC_UUID => :uuid_command,
LC_BUILD_VERSION => :build_version_command,
LC_SOURCE_VERSION => :source_version_command,
LC_MAIN => :entry_point_command,
LC_LOAD_DYLIB => :dylib_command,
LC_FUNCTION_STARTS => :linkedit_data_command,
LC_DATA_IN_CODE => :linkedit_data_command,
LC_CODE_SIGNATURE => :linkedit_data_command,
}
StructDefinition = {
mach_header_64: [
{name: 'magic', type: :uint32_t},
{name: 'cputype', type: :int32_t},
{name: 'cpusubtype', type: :int32_t},
{name: 'filetype', type: :uint32_t},
{name: 'ncmds', type: :uint32_t},
{name: 'sizeofcmds', type: :uint32_t},
{name: 'flags', type: :uint32_t},
{name: 'reserved', type: :uint32_t},
],
load_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
],
segment_command_64: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'segname', type: :string, bytes: 16},
{name: 'vmaddr', type: :uint64_t},
{name: 'vmsize', type: :uint64_t},
{name: 'fileoff', type: :uint64_t},
{name: 'filesize', type: :uint64_t},
{name: 'maxprot', type: :int32_t},
{name: 'initprot', type: :int32_t},
{name: 'nsects', type: :uint32_t},
{name: 'flags', type: :uint32_t},
],
linkedit_data_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'dataoff', type: :uint32_t},
{name: 'datasize', type: :uint32_t},
],
section_64: [
{name: 'sectname', type: :string, bytes: 16},
{name: 'segname', type: :string, bytes: 16},
{name: 'addr', type: :uint64_t},
{name: 'size', type: :uint64_t},
{name: 'offset', type: :uint32_t},
{name: 'align', type: :uint32_t},
{name: 'reloff', type: :uint32_t},
{name: 'nreloc', type: :uint32_t},
{name: 'flags', type: :uint32_t},
{name: 'reserved1', type: :uint32_t},
{name: 'reserved2', type: :uint32_t},
{name: 'reserved3', type: :uint32_t},
],
symtab_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'symoff', type: :uint32_t},
{name: 'nsyms', type: :uint32_t},
{name: 'stroff', type: :uint32_t},
{name: 'strsize', type: :uint32_t},
],
dysymtab_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'ilocalsym', type: :uint32_t},
{name: 'nlocalsym', type: :uint32_t},
{name: 'iextdefsym', type: :uint32_t},
{name: 'nextdefsym', type: :uint32_t},
{name: 'iundefsym', type: :uint32_t},
{name: 'nundefsym', type: :uint32_t},
{name: 'tocoff', type: :uint32_t},
{name: 'ntoc', type: :uint32_t},
{name: 'modtaboff', type: :uint32_t},
{name: 'nmodtab', type: :uint32_t},
{name: 'extrefsymoff', type: :uint32_t},
{name: 'nextrefsyms', type: :uint32_t},
{name: 'indirectsymoff', type: :uint32_t},
{name: 'nindirectsyms', type: :uint32_t},
{name: 'extreloff', type: :uint32_t},
{name: 'nextrel', type: :uint32_t},
{name: 'locreloff', type: :uint32_t},
{name: 'nlocrel', type: :uint32_t},
],
dylinker_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'name', type: :uint32_t},
],
uuid_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'uuid', type: :string, bytes: 16},
],
build_version_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'platform', type: :uint32_t},
{name: 'minos', type: :uint32_t},
{name: 'sdk', type: :uint32_t},
{name: 'ntools', type: :uint32_t},
],
source_version_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'version', type: :uint64_t},
],
entry_point_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'entryoff', type: :uint64_t},
{name: 'stacksize', type: :uint64_t},
],
dylib_command: [
{name: 'cmd', type: :cmd_t},
{name: 'cmdsize', type: :uint32_t},
{name: 'name', type: :uint32_t},
{name: 'timestamp', type: :uint32_t},
{name: 'current_version', type: :uint32_t},
{name: 'compatibility_version', type: :uint32_t},
],
}
TypeBytes = {
uint32_t: 4,
int32_t: 4,
uint64_t: 8,
int64_t: 8,
cmd_t: 4,
}
class MachAnalyzer
def initialize()
@header = nil
@loadCommands = []
end
def analyze(fileName)
self.setUpFile(fileName)
@header = self.readStruct(:mach_header_64)
if !self.isHeaderLegal(@header)
console.error('Error: not a Mach-O file')
process.exit(1)
end
dumpStruct(@header)
@loadCommands = []
ncmds = @header[:ncmds]
sections = []
blocks = []
ncmds.times do |i|
command = self.readLoadCommand()
@loadCommands << command
dumpStruct(command)
case command[:_structName]
when :segment_command_64
command[:nsects].times do |i|
section = self.readStruct(:section_64)
sections << section
dumpStruct(section)
blocks << {offset: section[:offset], size: section[:size], type: section[:sectname]}
if section[:nreloc] > 0
blocks << {offset: section[:reloff], size: section[:nreloc] * 8, type: :reloc}
end
end
when :linkedit_data_command
if command[:datasize] > 0
blocks << {offset: command[:dataoff], size: command[:datasize], type: CommandNameTable[command[:cmd]]}
end
when :symtab_command
blocks << {offset: command[:symoff], size: 0x10 * command[:nsyms], type: command[:cmd]}
blocks << {offset: command[:stroff], size: command[:strsize], type: :string}
when :dysymtab_command
if command[:indirectsymoff] != 0
blocks << {offset: command[:indirectsymoff], size: 0x04 * command[:nindirectsyms], type: :indirectsym}
end
end
next_offset = command[:_startOffset] + command[:cmdsize]
if next_offset > @offset
dumpHex(@data[@offset, next_offset - @offset], @offset)
end
@offset = next_offset
end
blocks.sort_by! {|block| block[:offset]}
blocks.each do |block|
name = CommandNameTable[block[:type]] || block[:type]
offset = block[:offset]
size = block[:size]
if offset > @offset
# Check whether data is empty.
at = (@offset...offset).find {|i| @data[i].ord != 0}
if at
$stderr.puts("Warning: data is not empty between 0x#{@offset.to_s(16)} and 0x#{offset.to_s(16)}")
end
end
puts
dumpHex(@data[offset, size], offset)
puts "# #{name}"
@offset = offset + size
end
end
def setUpFile(fileName)
bin = File.open(fileName, 'rb').read
@data = bin
@offset = 0
end
def isHeaderLegal(header)
magic64 = 0xfeedfacf
return header[:magic] === magic64
end
def dumpStruct(element)
puts
dumpHex(element[:_rawBytes], element[:_startOffset])
puts("# struct #{element[:_structName]}")
structMembers = StructDefinition[element[:_structName]]
structMembers.each do |member|
name, type = member[:name], member[:type]
unless /^(reserved|_)/ =~ name.to_s
value = element[name.to_sym]
case type
when :uint32_t, :int32_t
value = "0x#{value.to_s(16).rjust(8, '0')}"
when :uint64_t, :int64_t
value = "0x#{value.to_s(16).rjust(16, '0')}"
when :cmd_t
value = "#{CommandNameTable[value]} (0x#{value.to_s(16)})"
when :string
# value = value.each_byte.map {|b| 0x20<=b&&b<=0x7e ? b.chr : "\\#{b.to_s(16).rjust(2, '0')}"}.join('')
value = value.inspect
else
raise "Unknown type: #{type}"
end
puts("# .#{name}: #{value},")
end
end
end
def dumpHex(array, start)
line_bytes = 16
bytes = array.length
offset = 0
while offset < bytes
s = 0
if offset == 0
s = start % line_bytes
n = [bytes, line_bytes - s].min
else
n = [bytes - offset, line_bytes].min
end
bin = (0...n).map {|i| array[offset + i].unpack('C')[0].to_s(16).rjust(2, '0')}.join(' ')
chr = (0...n).map {|i| toChar(array[offset + i].ord)}.join('')
if s > 0
bin = ' ' * s + bin
chr = ' ' * s + chr
end
spaces = ' ' * (line_bytes * 3 + 1 - bin.length)
address = (start + offset).to_s(16).rjust(8, '0')
puts("#{address}: #{bin}#{spaces}#{chr}")
offset += n
end
end
def toChar(byte)
return byte >= 0x20 && byte <= 0x7e ? byte.chr : '.'
end
def readLoadCommand()
# ロードコマンドのヘッダ部分を読み込み、コマンドタイプを調べる
offset = @offset
loadCommand = self.readStruct(:load_command)
loadCommandType = LoadCommandStructName[loadCommand[:cmd]]
if !loadCommandType
raise "Unknown load command: #{loadCommand[:cmd]}"
end
# 実際の構造体として再読み込み
@offset = offset
return self.readStruct(loadCommandType)
end
def readStruct(structName)
structMembers = StructDefinition[structName]
if !structMembers
raise "Unknown struct: #{structName}"
end
offset = @offset
result = {}
structMembers.each do |member|
bytes = member[:type] == :string ? member[:bytes] : TypeBytes[member[:type]]
result[member[:name].to_sym] = MachAnalyzer.readValue(@data, offset, member)
offset += bytes
end
result[:_structName] = structName
result[:_startOffset] = @offset
result[:_rawBytes] = @data[result[:_startOffset], offset - result[:_startOffset]]
@offset = offset
return result
end
def self.readValue(data, offset, member)
case member[:type]
when :uint32_t
return getUint32(data, offset)
when :int32_t
return getInt32(data, offset)
when :uint64_t
return getUint64(data, offset)
when :int64_t
return getInt64(data, offset)
when :string
s = data[offset...(offset + member[:bytes])]
return s.gsub(/\0.*/, '')
when :cmd_t
return getUint32(data, offset)
else
raise "Unknown type: #{member.type}"
end
end
def self.getUint32(data, offset)
data[offset, 4].unpack('L')[0]
end
def self.getInt32(data, offset)
data[offset, 4].unpack('l')[0]
end
def self.getUint64(data, offset)
data[offset, 8].unpack('Q')[0]
end
def self.getInt64(data, offset)
data[offset, 8].unpack('q')[0]
end
end
if $0 == __FILE__
args = ARGV
if args.length != 1
$stderr.puts('Usage: mach-analyzer <path-to-mach-o-file>')
exit(1)
end
analyzer = MachAnalyzer.new()
analyzer.analyze(args[0])
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment