]>
Commit | Line | Data |
---|---|---|
1 | import sys | |
2 | import macholib | |
3 | from macholib import MachO as macho | |
4 | from collections import namedtuple | |
5 | import re | |
6 | ||
7 | # some fixups in macholib that are required for kext support | |
8 | macholib.mach_o.MH_KEXT_BUNDLE = 0xB | |
9 | ||
10 | macholib.mach_o.MH_FILETYPE_NAMES[macholib.mach_o.MH_KEXT_BUNDLE] = "kext bundle" | |
11 | macholib.mach_o.MH_FILETYPE_SHORTNAMES[macholib.mach_o.MH_KEXT_BUNDLE] = "kext" | |
12 | ||
13 | _old_MachOHeader_load = macho.MachOHeader.load | |
14 | def new_load(s, fh): | |
15 | try: | |
16 | _old_MachOHeader_load(s, fh) | |
17 | except ValueError as e: | |
18 | if str(e.message).find('total_size > low_offset') >= 0: | |
19 | pass | |
20 | else: | |
21 | raise | |
22 | except Exception as e: | |
23 | raise | |
24 | macho.MachOHeader.load = new_load | |
25 | ||
26 | class MemFile(object): | |
27 | def __init__(self, memory, size): | |
28 | self._start = 0 | |
29 | self._readp = 0 | |
30 | self._end = size | |
31 | self._mem = memory | |
32 | ||
33 | def tell(self): | |
34 | return self._readp | |
35 | ||
36 | def check_bounds(self, seek_position, operation): | |
37 | if not (self._start <= seek_position <= self._end): | |
38 | raise IOError("%s to offset %d failed bounds check [%d, %d]" % ( | |
39 | operation, seek_position, self._start, self._end)) | |
40 | ||
41 | def seek(self, offset, whence=0): | |
42 | seekto = offset | |
43 | if whence == 0: | |
44 | seekto += self._start | |
45 | elif whence == 1: | |
46 | seekto += self.tell() | |
47 | elif whence == 2: | |
48 | seekto += self._end | |
49 | else: | |
50 | raise IOError("Invalid whence argument to seek: %r" % (whence,)) | |
51 | self.check_bounds(seekto, 'seek') | |
52 | self._readp = seekto | |
53 | ||
54 | def write(self, bytes): | |
55 | raise NotImplementedError('write is not supported') | |
56 | ||
57 | def read(self, size=sys.maxsize): | |
58 | if size < 0: | |
59 | raise ValueError("Invalid size {} while reading from {}".format(size, self._fileobj)) | |
60 | here = self.tell() | |
61 | self.check_bounds(here, 'read') | |
62 | bytes = min(size, self._end - here) | |
63 | retval = self._mem[self._readp:self._readp + bytes] | |
64 | self._readp += bytes | |
65 | return retval | |
66 | ||
67 | MachOSegment = namedtuple('MachOSegment', 'name vmaddr vmsize fileoff filesize') | |
68 | ||
69 | class MemMacho(macho.MachO): | |
70 | ||
71 | def __init__(self, memdata, size=None): | |
72 | if size is None: | |
73 | super(MemMacho,self).__init__(memdata) | |
74 | return | |
75 | # | |
76 | # supports the ObjectGraph protocol | |
77 | self.graphident = 'mem:%d//'.format(size) | |
78 | self.filename = 'mem:%d//'.format(size) | |
79 | ||
80 | # initialized by load | |
81 | self.fat = None | |
82 | self.headers = [] | |
83 | fp = MemFile(memdata, size) | |
84 | self.load(fp) | |
85 | ||
86 | ||
87 | def get_segments_with_name(self, filter_re): | |
88 | """ param: filter_re is a compiled re which will be matched against segment name. | |
89 | Use: '' to match anything and everything | |
90 | returns: [ MachOSegment, MachOSegment, ... ] | |
91 | """ | |
92 | if type(filter_re) is str: | |
93 | filter_re = re.compile(filter_re) | |
94 | retval = [] | |
95 | for h in self.headers: | |
96 | for cmd in h.commands: | |
97 | # cmds is [(load_command, segment, [sections..])] | |
98 | (lc, segment, sections) = cmd | |
99 | if isinstance(segment, SEGMENT_TYPES): | |
100 | segname = segment.segname[:segment.segname.find('\x00')] | |
101 | if filter_re.match(segname): | |
102 | retval.append(MachOSegment(segname, segment.vmaddr, segment.vmsize, segment.fileoff, segment.filesize)) | |
103 | return retval | |
104 | ||
105 | def get_sections_with_name(self, filter_re): | |
106 | """ param: filter_re is a compiled re which will be matched against <segment_name>.<section_name> | |
107 | Use: '' to match anything and everything | |
108 | returns: [ MachOSegment, MachOSegment, ... ] | |
109 | where each MachOSegment.name is <segment_name>.<section_name> | |
110 | """ | |
111 | if type(filter_re) is str: | |
112 | filter_re = re.compile(filter_re) | |
113 | retval = [] | |
114 | for h in self.headers: | |
115 | for cmd in h.commands: | |
116 | # cmds is [(load_command, segment, [sections..])] | |
117 | (lc, segment, sections) = cmd | |
118 | if isinstance(segment, SEGMENT_TYPES): | |
119 | segname = segment.segname[:segment.segname.find('\x00')] | |
120 | for section in sections: | |
121 | section_name = section.sectname[:section.sectname.find('\x00')] | |
122 | full_section_name= "{}.{}".format(segname, section_name) | |
123 | if filter_re.match(full_section_name): | |
124 | retval.append(MachOSegment(full_section_name, section.addr, section.size, section.offset, section.size)) | |
125 | return retval | |
126 | ||
127 | ||
128 | def get_uuid(self): | |
129 | retval = '' | |
130 | for h in self.headers: | |
131 | for cmd in h.commands: | |
132 | # cmds is [(load_command, segment, [sections..])] | |
133 | (lc, segment, sections) = cmd | |
134 | if isinstance(segment, macholib.mach_o.uuid_command): | |
135 | retval = GetUUIDSummary(segment.uuid) | |
136 | return retval | |
137 | ||
138 | def get_text_segment(segments): | |
139 | retval = None | |
140 | for s in segments: | |
141 | if s.name == '__TEXT_EXEC': | |
142 | return s | |
143 | for s in segments: | |
144 | if s.name == '__TEXT': | |
145 | return s | |
146 | return retval | |
147 | ||
148 | def get_segment_with_addr(segments, addr): | |
149 | """ param: segments [MachOSegment, ...] | |
150 | return: None or MachOSegment where addr is in vmaddr...(vmaddr+vmsize) | |
151 | """ | |
152 | for s in segments: | |
153 | if addr >= s.vmaddr and addr < (s.vmaddr + s.vmsize): | |
154 | return s | |
155 | return None | |
156 | ||
157 | def GetUUIDSummary(arr): | |
158 | data = [] | |
159 | for i in range(16): | |
160 | data.append(ord(arr[i])) | |
161 | return "{a[0]:02X}{a[1]:02X}{a[2]:02X}{a[3]:02X}-{a[4]:02X}{a[5]:02X}-{a[6]:02X}{a[7]:02X}-{a[8]:02X}{a[9]:02X}-{a[10]:02X}{a[11]:02X}{a[12]:02X}{a[13]:02X}{a[14]:02X}{a[15]:02X}".format(a=data) | |
162 | ||
163 | SEGMENT_TYPES = (macholib.mach_o.segment_command_64, macholib.mach_o.segment_command) | |
164 | ||
165 | def get_load_command_human_name(cmd): | |
166 | """ return string name of LC_LOAD_DYLIB => "load_dylib" | |
167 | "<unknown>" if not found | |
168 | """ | |
169 | retval = "<unknown>" | |
170 | if cmd in macho.LC_REGISTRY: | |
171 | retval = macho.LC_REGISTRY[cmd].__name__ | |
172 | retval = retval.replace("_command","") | |
173 | return retval | |
174 | ||
175 | class VisualMachoMap(object): | |
176 | KB_1 = 1024 | |
177 | KB_16 = 16 * 1024 | |
178 | MB_1 = 1 * 1024 * 1024 | |
179 | GB_1 = 1 * 1024 * 1024 * 1024 | |
180 | ||
181 | def __init__(self, name, width=40): | |
182 | self.name = name | |
183 | self.width = 40 | |
184 | self.default_side_padding = 2 | |
185 | ||
186 | def get_header_line(self): | |
187 | return '+' + '-' * (self.width - 2) + '+' | |
188 | ||
189 | def get_space_line(self): | |
190 | return '|' + ' ' * (self.width - 2) + '|' | |
191 | ||
192 | def get_dashed_line(self): | |
193 | return '|' + '-' * (self.width - 2) + '|' | |
194 | ||
195 | def get_dotted_line(self): | |
196 | return '|' + '.' * (self.width - 2) + '|' | |
197 | ||
198 | def center_text_in_line(self, line, text): | |
199 | even_length = bool(len(text) % 2 == 0) | |
200 | if len(text) > len(line) - 2: | |
201 | raise ValueError("text is larger than line of text") | |
202 | ||
203 | lbreak_pos = len(line)/2 - len(text)/2 | |
204 | if not even_length: | |
205 | lbreak_pos -= 1 | |
206 | out = line[:lbreak_pos] + text | |
207 | return out + line[len(out):] | |
208 | ||
209 | def get_separator_lines(self): | |
210 | return ['/' + ' ' * (self.width - 2) + '/', '/' + ' ' * (self.width - 2) + '/'] | |
211 | ||
212 | def printMachoMap(self, mobj): | |
213 | MapBlock = namedtuple('MapBlock', 'name vmaddr vmsize fileoff filesize extra_info is_segment') | |
214 | outstr = self.name + '\n' | |
215 | other_cmds = '' | |
216 | blocks = [] | |
217 | for hdr in mobj.headers: | |
218 | cmd_index = 0 | |
219 | for cmd in hdr.commands: | |
220 | # cmds is [(load_command, segment, [sections..])] | |
221 | (lc, segment, sections) = cmd | |
222 | lc_cmd_str = get_load_command_human_name(lc.cmd) | |
223 | lc_str_rep = "\n\t LC: {:s} size:{:d} nsects:{:d}".format(lc_cmd_str, lc.cmdsize, len(sections)) | |
224 | # print lc_str_rep | |
225 | if isinstance(segment, SEGMENT_TYPES): | |
226 | segname = segment.segname[:segment.segname.find('\x00')] | |
227 | # print "\tsegment: {:s} vmaddr: {:x} vmsize:{:d} fileoff: {:x} filesize: {:d}".format( | |
228 | # segname, segment.vmaddr, segment.vmsize, segment.fileoff, segment.filesize) | |
229 | blocks.append(MapBlock(segname, segment.vmaddr, segment.vmsize, segment.fileoff, segment.filesize, | |
230 | ' LC:{} : {} init:{:#0X} max:{:#0X}'.format(lc_cmd_str, segname, segment.initprot, segment.maxprot), | |
231 | True)) | |
232 | for section in sections: | |
233 | section_name = section.sectname[:section.sectname.find('\x00')] | |
234 | blocks.append(MapBlock(section_name, section.addr, section.size, section.offset, | |
235 | section.size, 'al:{} flags:{:#0X}'.format(section.align, section.flags), False)) | |
236 | #print "\t\tsection:{:s} addr:{:x} off:{:x} size:{:d}".format(section_name, section.addr, section.offset, section.size) | |
237 | elif isinstance(segment, macholib.mach_o.uuid_command): | |
238 | other_cmds += "\n\t uuid: {:s}".format(GetUUIDSummary(segment.uuid)) | |
239 | elif isinstance(segment, macholib.mach_o.rpath_command): | |
240 | other_cmds += "\n\t rpath: {:s}".format(segment.path) | |
241 | elif isinstance(segment, macholib.mach_o.dylib_command): | |
242 | other_cmds += "\n\t dylib: {:s} ({:s})".format(str(sections[:sections.find('\x00')]), str(segment.current_version)) | |
243 | else: | |
244 | other_cmds += lc_str_rep | |
245 | cmd_index += 1 | |
246 | ||
247 | # fixup the self.width param | |
248 | for _b in blocks: | |
249 | if self.default_side_padding + len(_b.name) + 2 > self.width: | |
250 | self.width = self.default_side_padding + len(_b.name) + 2 | |
251 | if self.width % 2 != 0: | |
252 | self.width += 1 | |
253 | ||
254 | sorted_blocks = sorted(blocks, key=lambda b: b.vmaddr) | |
255 | mstr = [self.get_header_line()] | |
256 | prev_block = MapBlock('', 0, 0, 0, 0, '', False) | |
257 | for b in sorted_blocks: | |
258 | # TODO add separator blocks if vmaddr is large from prev_block | |
259 | if b.is_segment: | |
260 | s = self.get_dashed_line() | |
261 | else: | |
262 | s = self.get_dotted_line() | |
263 | s = self.center_text_in_line(s, b.name) | |
264 | line = "{:s} {: <#020X} ({: <10d}) floff:{: <#08x} {}".format(s, b.vmaddr, b.vmsize, b.fileoff, b.extra_info) | |
265 | if (b.vmaddr - prev_block.vmaddr) > VisualMachoMap.KB_16: | |
266 | mstr.append(self.get_space_line()) | |
267 | mstr.append(self.get_space_line()) | |
268 | ||
269 | mstr.append(line) | |
270 | ||
271 | if b.vmsize > VisualMachoMap.MB_1: | |
272 | mstr.append(self.get_space_line()) | |
273 | mstr.extend(self.get_separator_lines()) | |
274 | mstr.append(self.get_space_line()) | |
275 | #mstr.append(self.get_space_line()) | |
276 | prev_block = b | |
277 | mstr.append(self.get_space_line()) | |
278 | if prev_block.vmsize > VisualMachoMap.KB_16: | |
279 | mstr.append(self.get_space_line()) | |
280 | mstr.append(self.get_header_line()) | |
281 | print outstr | |
282 | print "\n".join(mstr) | |
283 | print "\n\n=============== Other Load Commands ===============" | |
284 | print other_cmds | |
285 | ||
286 | ||
287 | if __name__ == '__main__': | |
288 | import sys | |
289 | if len(sys.argv) < 2: | |
290 | print "Usage: {} /path/to/macho_binary".format(sys.argv[0]) | |
291 | sys.exit(1) | |
292 | with open(sys.argv[-1], 'rb') as fp: | |
293 | data = fp.read() | |
294 | mobject = MemMacho(data, len(data)) | |
295 | ||
296 | p = VisualMachoMap(sys.argv[-1]) | |
297 | p.printMachoMap(mobject) | |
298 | sys.exit(0) | |
299 |