Last active
March 26, 2025 00:02
-
-
Save tyfkda/d61eb12fd663c574ca87387b2f31aa39 to your computer and use it in GitHub Desktop.
Mach-Oファイルを解析するツール(Ruby)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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