""" Tools for manipulating Portable Executable files. This can be used, for example, to extract a list of dependencies from an .exe or .dll file, or to add version information and an icon resource to it. """ __all__ = ["PEFile"] from struct import Struct, unpack, pack, pack_into from collections import namedtuple from array import array import time from io import BytesIO import sys if sys.version_info >= (3, 0): unicode = str unichr = chr # Define some internally used structures. RVASize = namedtuple('RVASize', ('addr', 'size')) impdirtab = namedtuple('impdirtab', ('lookup', 'timdat', 'forward', 'name', 'impaddr')) expdirtab = namedtuple('expdirtab', ('flags', 'timdat', 'majver', 'minver', 'name', 'ordinal_base', 'nentries', 'nnames', 'entries', 'names', 'ordinals')) def _unpack_zstring(mem, offs=0): "Read a zero-terminated string from memory." c = mem[offs] str = "" while c: str += chr(c) offs += 1 c = mem[offs] return str def _unpack_wstring(mem, offs=0): "Read a UCS-2 string from memory." name_len, = unpack('" % (self.name, self.vaddr, self.vaddr + self.vsize) def __gt__(self, other): return self.vaddr > other.vaddr def __lt__(self, other): return self.vaddr < other.vaddr class DataResource(object): """ A resource entry in the resource table. """ # Resource types. cursor = 1 bitmap = 2 icon = 3 menu = 4 dialog = 5 string = 6 font_directory = 7 font = 8 accelerator = 9 rcdata = 10 message_table = 11 cursor_group = 12 icon_group = 14 version = 16 dlg_include = 17 plug_play = 19 vxd = 20 animated_cursor = 21 animated_icon = 22 html = 23 manifest = 24 def __init__(self): self._ident = () self.data = None self.code_page = 0 @property def encoding(self): if self.code_page == 0: return 'ascii' else: return 'cp%d' % (self.code_page) def get_data(self): return self.data def get_text(self, errors='strict'): return self.data.decode(self.encoding, errors) class IconGroupResource(object): code_page = 0 type = 14 _entry = Struct('= 256: colors = 0 if width >= 256: width = 0 if height >= 256: height = 0 data += self._entry.pack(width, height, colors, planes, bpp, size, id) return data def unpack_from(self, data, offs=0): type, count = unpack(' 0: self.signature = dwords[0] if len(dwords) > 1: self.struct_version = dwords[1] if len(dwords) > 3: self.file_version = \ (int(dwords[2] >> 16), int(dwords[2] & 0xffff), int(dwords[3] >> 16), int(dwords[3] & 0xffff)) if len(dwords) > 5: self.product_version = \ (int(dwords[4] >> 16), int(dwords[4] & 0xffff), int(dwords[5] >> 16), int(dwords[5] & 0xffff)) if len(dwords) > 7: self.file_flags_mask = dwords[6] self.file_flags = dwords[7] if len(dwords) > 8: self.file_os = dwords[8] if len(dwords) > 9: self.file_type = dwords[9] if len(dwords) > 10: self.file_subtype = dwords[10] if len(dwords) > 12: self.file_date = (dwords[11], dwords[12]) while offset < length: offset += self._unpack_info(self, data, offset) def __getitem__(self, key): if key == 'StringFileInfo': return self.string_info elif key == 'VarFileInfo': return self.var_info else: raise KeyError("%s does not exist" % (key)) def __contains__(self, key): return key in ('StringFileInfo', 'VarFileInfo') def _unpack_info(self, dict, data, offset): length, value_length, type = unpack(' 0 end = offset + length offset += 6 key = "" c, = unpack(' 0: # It contains a value. if type: # It's a wchar array value. value = u"" c, = unpack('= key: if key == idname: return leaf break i += 1 if not isinstance(key, int): self._strings_size += _padded(len(key) * 2 + 2, 4) leaf = ResourceTable(ident=self._ident + (key,)) leaves.insert(i, (key, leaf)) return leaf def __setitem__(self, key, value): """ Adds the given item to the table. Maintains sort order. """ if isinstance(key, int): leaves = self._id_leaves else: leaves = self._name_leaves if not isinstance(value, ResourceTable): self._descs_size += 16 value._ident = self._ident + (key,) i = 0 while i < len(leaves): idname, leaf = leaves[i] if idname >= key: if key == idname: if not isinstance(leaves[i][1], ResourceTable): self._descs_size -= 16 leaves[i] = (key, value) return break i += 1 if not isinstance(key, int): self._strings_size += _padded(len(key) * 2 + 2, 4) leaves.insert(i, (key, value)) def __len__(self): return len(self._name_leaves) + len(self._id_leaves) def __iter__(self): keys = [] for name, leaf in self._name_leaves: keys.append(name) for id, leaf in self._id_leaves: keys.append(id) return iter(keys) def items(self): return self._name_leaves + self._id_leaves def count_resources(self): """Counts all of the resources.""" count = 0 for key, leaf in self._name_leaves + self._id_leaves: if isinstance(leaf, ResourceTable): count += leaf.count_resources() else: count += 1 return count def get_nested_tables(self): """Returns all tables in this table and subtables.""" # First we yield child tables, then nested tables. This is the # order in which pack_into assumes the tables will be written. for key, leaf in self._name_leaves + self._id_leaves: if isinstance(leaf, ResourceTable): yield leaf for key, leaf in self._name_leaves + self._id_leaves: if isinstance(leaf, ResourceTable): for table in leaf.get_nested_tables(): yield table def pack_header(self, data, offs): self._header.pack_into(data, offs, self.flags, self.timdat, self.version[0], self.version[1], len(self._name_leaves), len(self._id_leaves)) def unpack_from(self, mem, addr=0, offs=0): start = addr + offs self.flags, self.timdat, majver, minver, nnames, nids = \ self._header.unpack(mem[start:start+16]) self.version = (majver, minver) start += 16 # Subtables/entries specified by string name. self._name_leaves = [] for i in range(nnames): name_p, data = unpack('= 1: self.exp_rva = RVASize(*unpack('= 2: self.imp_rva = RVASize(*unpack('= 3: self.res_rva = RVASize(*unpack('= 4: fp.seek((numrvas - 3) * 8, 1) # Loop through the sections to find the ones containing our tables. self.sections = [] for i in range(nscns): section = Section() section.read_header(fp) self.sections.append(section) self.sections.sort() # Read the sections into some kind of virtual memory. self.vmem = bytearray(self.sections[-1].vaddr + self.sections[-1].size) memview = memoryview(self.vmem) for section in self.sections: fp.seek(section.offset) fp.readinto(memview[section.vaddr:section.vaddr+section.size]) # Read the import table. start = self.imp_rva.addr dir = impdirtab(*unpack('= 0 and ordinal < expdir.nentries start = expdir.entries + 4 * ordinal addr, = unpack('= section.vaddr and addr < section.vaddr + section.size: return section def add_icon(self, icon, ordinal=2): """ Adds an icon resource from the given Icon object. Requires calling add_resource_section() afterwards. """ group = IconGroupResource() self.resources[group.type][ordinal][1033] = group images = sorted(icon.images.items(), key=lambda x:-x[0]) id = 1 # Write 8-bpp image headers for sizes under 256x256. for size, image in images: if size >= 256: continue xorsize = size if xorsize % 4 != 0: xorsize += 4 - (xorsize % 4) andsize = (size + 7) >> 3 if andsize % 4 != 0: andsize += 4 - (andsize % 4) datasize = 40 + 256 * 4 + (xorsize + andsize) * size group.add_icon(size, size, 1, 8, datasize, id) buf = BytesIO() icon._write_bitmap(buf, image, size, 8) res = DataResource() res.data = buf.getvalue() self.resources[3][id][1033] = res id += 1 # And now the 24/32 bpp versions. for size, image in images: if size > 256: continue # Calculate the size so we can write the offset within the file. if image.hasAlpha(): bpp = 32 xorsize = size * 4 else: bpp = 24 xorsize = size * 3 + (-(size * 3) & 3) andsize = (size + 7) >> 3 if andsize % 4 != 0: andsize += 4 - (andsize % 4) datasize = 40 + (xorsize + andsize) * size buf = BytesIO() icon._write_bitmap(buf, image, size, bpp) res = DataResource() res.data = buf.getvalue() self.resources[3][id][1033] = res group.add_icon(size, size, 1, bpp, datasize, id) id += 1 def add_section(self, name, flags, data): """ Adds a new section with the given name, flags and data. The virtual address space is automatically resized to fit the new data. Returns the newly created Section object. """ if isinstance(name, unicode): name = name.encode('ascii') section = Section() section.name = name section.flags = flags # Put it at the end of all the other sections. section.offset = 0 for s in self.sections: section.offset = max(section.offset, s.offset + s.size) # Align the offset. section.offset = _padded(section.offset, self.file_alignment) # Find a place to put it in the virtual address space. section.vaddr = len(self.vmem) align = section.vaddr % self.section_alignment if align: pad = self.section_alignment - align self.vmem += bytearray(pad) section.vaddr += pad section.vsize = len(data) section.size = _padded(section.vsize, self.file_alignment) self.vmem += data self.sections.append(section) # Update the size tallies from the opthdr. self.image_size += _padded(section.vsize, self.section_alignment) if flags & 0x20: self.code_size += section.size if flags & 0x40: self.initialized_size += section.size if flags & 0x80: self.uninitialized_size += section.size return section def add_version_info(self, file_ver, product_ver, data, lang=1033, codepage=1200): """ Adds a version info resource to the file. """ if "FileVersion" not in data: data["FileVersion"] = '.'.join(file_ver) if "ProductVersion" not in data: data["ProductVersion"] = '.'.join(product_ver) assert len(file_ver) == 4 assert len(product_ver) == 4 res = VersionInfoResource() res.file_version = file_ver res.product_version = product_ver res.string_info = { "%04x%04x" % (lang, codepage): data } res.var_info = { "Translation": bytearray(pack("= 3 fp.seek(self.rva_offset + 4) if numrvas >= 1: fp.write(pack('= 2: fp.write(pack('= 3: fp.write(pack('= 4: fp.seek((numrvas - 3) * 8, 1) # Write the modified section headers. for section in self.sections: section.write_header(fp) assert fp.tell() <= self.header_size # Write the section data of modified sections. for section in self.sections: if not section.modified: continue fp.seek(section.offset) size = min(section.vsize, section.size) fp.write(self.vmem[section.vaddr:section.vaddr+size]) pad = section.size - size assert pad >= 0 if pad > 0: fp.write(bytearray(pad)) section.modified = False