Class: MachO::MachOFile

Inherits:
Object
  • Object
show all
Defined in:
lib/macho/macho_file.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(filename) ⇒ MachOFile

Returns a new instance of MachOFile.

Raises:

  • (ArgumentError)


12
13
14
15
16
17
18
19
# File 'lib/macho/macho_file.rb', line 12

def initialize(filename)
	raise ArgumentError.new("filename must be a String") unless filename.is_a? String

	@filename = filename
	@raw_data = open(@filename, "rb") { |f| f.read }
	@header = get_mach_header
	@load_commands = get_load_commands
end

Instance Attribute Details

#headerObject (readonly)

Returns the value of attribute header.



3
4
5
# File 'lib/macho/macho_file.rb', line 3

def header
  @header
end

#load_commandsObject (readonly)

Returns the value of attribute load_commands.



3
4
5
# File 'lib/macho/macho_file.rb', line 3

def load_commands
  @load_commands
end

Class Method Details

.new_from_bin(bin) ⇒ Object



5
6
7
8
9
10
# File 'lib/macho/macho_file.rb', line 5

def self.new_from_bin(bin)
	instance = allocate
	instance.initialize_from_bin(bin)

	instance
end

Instance Method Details

#bundle?Boolean

is the file a dynamically bound bundle?

Returns:

  • (Boolean)


51
52
53
# File 'lib/macho/macho_file.rb', line 51

def bundle?
	header[:filetype] == MH_BUNDLE
end

#change_dylib!(old_path, new_path) ⇒ Object

stub

Raises:



221
222
223
# File 'lib/macho/macho_file.rb', line 221

def change_dylib!(old_path, new_path)
	raise DylibUnknownError.new(old_path) unless linked_dylibs.include?(old_path)
end

#command(name) ⇒ Object Also known as: []

get load commands by name



95
96
97
# File 'lib/macho/macho_file.rb', line 95

def command(name)
	load_commands.select { |lc| lc.to_s == name }
end

#cpusubtypeObject

string representation of the header’s cpusubtype field



75
76
77
# File 'lib/macho/macho_file.rb', line 75

def cpusubtype
	CPU_SUBTYPES[header[:cpusubtype]]
end

#cputypeObject

string representation of the header’s cputype field



70
71
72
# File 'lib/macho/macho_file.rb', line 70

def cputype
	CPU_TYPES[header[:cputype]]
end

#dylib?Boolean

is the file a dynamically bound shared object?

Returns:

  • (Boolean)


46
47
48
# File 'lib/macho/macho_file.rb', line 46

def dylib?
	header[:filetype] == MH_DYLIB
end

#dylib_idObject

get the file’s dylib id, if it is a dylib



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/macho/macho_file.rb', line 111

def dylib_id
	if !dylib?
		return nil
	end

	dylib_id_cmd = command('LC_ID_DYLIB').first

	cmdsize = dylib_id_cmd.cmdsize
	offset = dylib_id_cmd.offset
	stroffset = dylib_id_cmd.name

	dylib_id = @raw_data.slice(offset + stroffset...offset + cmdsize).unpack("Z*").first

	dylib_id.delete("\x00")
end

#dylib_id=(new_id) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/macho/macho_file.rb', line 127

def dylib_id=(new_id)
	if !new_id.is_a?(String)
		raise ArgumentError.new("argument must be a String")
	end

	if !dylib?
		return nil
	end

	if magic32?
		cmd_round = 4
	else
		cmd_round = 8
	end

	new_sizeofcmds = header[:sizeofcmds]
	dylib_id_cmd = command('LC_ID_DYLIB').first
	old_id = dylib_id
	new_id = new_id.dup

	new_pad = MachO.round(new_id.size, cmd_round) - new_id.size
	old_pad = MachO.round(old_id.size, cmd_round) - old_id.size

	# pad the old and new IDs with null bytes to meet command bounds
	old_id << "\x00" * old_pad
	new_id << "\x00" * new_pad

	# calculate the new size of the DylibCommand and sizeofcmds in MH
	new_size = DylibCommand.bytesize + new_id.size
	new_sizeofcmds += new_size - dylib_id_cmd.cmdsize

	# calculate the low file offset (offset to first section data)
	low_fileoff = 2**64 # ULLONGMAX

	segments.each do |seg|
		sections(seg).each do |sect|
			if sect.size != 0 && !sect.flag?(S_ZEROFILL) &&
					!sect.flag?(S_THREAD_LOCAL_ZEROFILL) &&
					sect.offset < low_fileoff

				low_fileoff = sect.offset
			end
		end
	end

	if new_sizeofcmds + header.bytesize > low_fileoff
		raise HeaderPadError.new(@filename)
	end

	# update sizeofcmds in mach_header
	set_sizeofcmds(new_sizeofcmds)

	# update cmdsize in the dylib_command
	@raw_data[dylib_id_cmd.offset + 4, 4] = [new_size].pack("V")

	# delete the old id
	@raw_data.slice!(dylib_id_cmd.offset + dylib_id_cmd.name...dylib_id_cmd.offset + dylib_id_cmd.class.bytesize + old_id.size)

	# insert the new id
	@raw_data.insert(dylib_id_cmd.offset + dylib_id_cmd.name, new_id)

	# pad/unpad after new_sizeofcmds until offsets are corrected
	null_pad = old_id.size - new_id.size

	if null_pad < 0
		@raw_data.slice!(new_sizeofcmds + header.bytesize, null_pad.abs)
	else
		@raw_data.insert(new_sizeofcmds + header.bytesize, "\x00" * null_pad)
	end

	# synchronize fields with the raw data
	header = get_mach_header
	load_commands = get_load_commands
end

#executable?Boolean

is the file executable?

Returns:

  • (Boolean)


41
42
43
# File 'lib/macho/macho_file.rb', line 41

def executable?
	header[:filetype] == MH_EXECUTE
end

#filetypeObject

string representation of the header’s filetype field



65
66
67
# File 'lib/macho/macho_file.rb', line 65

def filetype
	MH_FILETYPES[header[:filetype]]
end

#flagsObject

various execution flags



90
91
92
# File 'lib/macho/macho_file.rb', line 90

def flags
	header[:flags]
end

#initialize_from_bin(bin) ⇒ Object



21
22
23
24
25
26
# File 'lib/macho/macho_file.rb', line 21

def initialize_from_bin(bin)
	@filename = nil
	@raw_data = bin
	@header = get_mach_header
	@load_commands = get_load_commands
end

#linked_dylibsObject

get a list of dylib paths linked to this file



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/macho/macho_file.rb', line 203

def linked_dylibs
	dylibs = []
	dylib_cmds = command('LC_LOAD_DYLIB')

	dylib_cmds.each do |dylib_cmd|
		cmdsize = dylib_cmd.cmdsize
		offset = dylib_cmd.offset
		stroffset = dylib_cmd.name

		dylib = @raw_data.slice(offset + stroffset...offset + cmdsize).unpack("Z*").first

		dylibs << dylib
	end

	dylibs
end

#magicObject



55
56
57
# File 'lib/macho/macho_file.rb', line 55

def magic
	header[:magic]
end

#magic32?Boolean

Returns:

  • (Boolean)


32
33
34
# File 'lib/macho/macho_file.rb', line 32

def magic32?
	MachO.magic32?(header[:magic])
end

#magic64?Boolean

Returns:

  • (Boolean)


36
37
38
# File 'lib/macho/macho_file.rb', line 36

def magic64?
	MachO.magic64?(header[:magic])
end

#magic_stringObject

string representation of the header’s magic bytes



60
61
62
# File 'lib/macho/macho_file.rb', line 60

def magic_string
	MH_MAGICS[header[:magic]]
end

#ncmdsObject

number of load commands in the header



80
81
82
# File 'lib/macho/macho_file.rb', line 80

def ncmds
	header[:ncmds]
end

#sections(segment) ⇒ Object

get all sections in a segment by name



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/macho/macho_file.rb', line 226

def sections(segment)
	sections = []

	if !segment.is_a?(SegmentCommand) && !segment.is_a?(SegmentCommand64)
		raise ArgumentError.new("not a valid segment")
	end

	if segment.nsects.zero?
		return sections
	end

	offset = segment.offset + segment.class.bytesize

	segment.nsects.times do
		if segment.is_a? SegmentCommand
			sections << Section.new_from_bin(@raw_data.slice(offset, Section.bytesize))
			offset += Section.bytesize
		else
			sections << Section64.new_from_bin(@raw_data.slice(offset, Section64.bytesize))
			offset += Section64.bytesize
		end
	end

	sections
end

#segmentsObject

get all segment commands



102
103
104
105
106
107
108
# File 'lib/macho/macho_file.rb', line 102

def segments
	if magic32?
		command("LC_SEGMENT")
	else
		command("LC_SEGMENT_64")
	end
end

#serializeObject



28
29
30
# File 'lib/macho/macho_file.rb', line 28

def serialize
	@raw_data
end

#sizeofcmdsObject

size of all load commands



85
86
87
# File 'lib/macho/macho_file.rb', line 85

def sizeofcmds
	header[:sizeofcmds]
end

#write(filename) ⇒ Object



252
253
254
# File 'lib/macho/macho_file.rb', line 252

def write(filename)
	File.open(filename, "wb") { |f| f.write(@raw_data) }
end

#write!Object



256
257
258
259
260
261
262
# File 'lib/macho/macho_file.rb', line 256

def write!
	if @filename.nil?
		raise MachOError.new("cannot write to a default file when initialized from a binary string")
	else
		File.open(@filename, "wb") { |f| f.write(@raw_data) }
	end
end