├── VERSION ├── .gitignore ├── pe_tools ├── __init__.py ├── utils.py ├── cvinfo.py ├── struct3.py ├── pdb.py ├── rsrc.py ├── version_info.py ├── peresed.py └── pe_parser.py ├── .github └── workflows │ ├── build.yml │ └── release.yml ├── setup.py ├── LICENSE └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vscode/ 3 | *.egg-info/ 4 | build/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /pe_tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .pdb import parse_pdb 2 | from .pe_parser import * 3 | from .rsrc import * 4 | from .version_info import parse_version_info 5 | -------------------------------------------------------------------------------- /pe_tools/utils.py: -------------------------------------------------------------------------------- 1 | def align4(val): 2 | return (val + 3) & ~3 3 | 4 | def align8(val): 5 | return (val + 7) & ~7 6 | 7 | def align16(val): 8 | return (val + 15) & ~15 9 | -------------------------------------------------------------------------------- /pe_tools/cvinfo.py: -------------------------------------------------------------------------------- 1 | from .struct3 import Struct3, u16, u32 2 | 3 | S_PUB32 = 0x110e 4 | 5 | class CvRecHdr(Struct3): 6 | reclen: u16 7 | rectyp: u16 8 | 9 | class PUBSYM32(Struct3): 10 | pubsymflags: u32 11 | off: u32 12 | seg: u16 13 | # name: length_prefixed_str 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches-ignore: 5 | - release 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: avakar/derive-version@85d631f1a12b084b2592d1160b3c9154ceea7ea8 14 | id: version 15 | - run: sed -i 's/{version}/${{ steps.version.outputs.version }}+${{ github.sha }}/g' setup.py 16 | - run: python3 -m pip install setuptools wheel 17 | - run: python3 setup.py sdist bdist_wheel 18 | - uses: actions/upload-artifact@v1.0.0 19 | with: 20 | name: pe_tools-${{ steps.version.outputs.version }}+${{ github.sha }} 21 | path: dist 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from setuptools import setup 5 | import os 6 | 7 | top, _ = os.path.split(__file__) 8 | with open(os.path.join(top, 'VERSION'), 'r') as fin: 9 | version = fin.read().strip() + '+local' 10 | version = '{version}'.format(version=version) 11 | 12 | setup( 13 | name='pe_tools', 14 | version=version, 15 | 16 | url='https://github.com/avast/pe_tools', 17 | maintainer='Martin Vejnár', 18 | maintainer_email='martin.vejnar@avast.com', 19 | 20 | packages=['pe_tools'], 21 | install_requires=['grope'], 22 | 23 | entry_points={ 24 | 'console_scripts': [ 25 | 'peresed = pe_tools.peresed:main', 26 | ], 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - release 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: avakar/derive-version@85d631f1a12b084b2592d1160b3c9154ceea7ea8 14 | id: version 15 | - run: sed -i 's/{version}/${{ steps.version.outputs.version }}/g' setup.py 16 | - run: python3 -m pip install setuptools wheel 17 | - run: python3 -m pip install twine 18 | - run: python3 setup.py sdist bdist_wheel 19 | - uses: actions/upload-artifact@v1.0.0 20 | with: 21 | name: pe_tools-${{ steps.version.outputs.version }} 22 | path: dist 23 | - name: Upload to PyPI 24 | run: | 25 | python3 -m twine upload -u avakar -p "${{ secrets.pypi_password }}" dist/* 26 | - uses: avakar/tag-and-release@8f4b627f03fe59381267d3925d39191e27f44236 27 | with: 28 | tag_name: v${{ steps.version.outputs.version }} 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /pe_tools/struct3.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import struct 3 | 4 | class Annotation: 5 | def __init__(self, fmt, default): 6 | self._mult = 1 7 | self._fmt = fmt 8 | self.default = default 9 | 10 | def __getitem__(self, key): 11 | r = Annotation(self._fmt, self.default) 12 | r._mult = self._mult * key 13 | return r 14 | 15 | @property 16 | def fmt(self): 17 | if self._mult == 1: 18 | return self._fmt 19 | else: 20 | return '{}{}'.format(self._mult, self._fmt) 21 | 22 | class StructDescriptor: 23 | def __init__(self, annotations): 24 | self.annotations = annotations 25 | 26 | fmt = ['<'] 27 | fmt.extend(annot.fmt for annot in annotations.values()) 28 | self.fmt = ''.join(fmt) 29 | self.size = struct.calcsize(self.fmt) 30 | 31 | @property 32 | def names(self): 33 | return self.annotations.keys() 34 | 35 | class StructMeta(type): 36 | def __new__(cls, name, bases, namespace, no_struct_members=False, **kwds): 37 | self = super().__new__(cls, name, bases, namespace, **kwds) 38 | 39 | if not no_struct_members: 40 | self.descriptor = StructDescriptor(namespace['__annotations__']) 41 | for name, annot in self.descriptor.annotations.items(): 42 | setattr(self, name, annot.default) 43 | self.size = self.descriptor.size 44 | 45 | return self 46 | 47 | class Struct3(metaclass=StructMeta, no_struct_members=True): 48 | descriptor: StructDescriptor 49 | size: int 50 | 51 | def __init__(self, *args, **kw): 52 | annots = self.descriptor.annotations 53 | 54 | if len(args) > 1: 55 | raise TypeError('{}() takes at most a single argument'.format(type(self).__name__)) 56 | 57 | if len(args) == 1: 58 | src = args[0] 59 | for k, v in src.__dict__.items(): 60 | if k not in annots: 61 | raise TypeError('source object contains an unexpected member {!r}'.format(k)) 62 | setattr(self, k, v) 63 | 64 | for k, v in kw.items(): 65 | if k not in annots: 66 | raise TypeError('{}() got an unexpected keyword argument {!r}'.format(type(self).__name__, k)) 67 | setattr(self, k, v) 68 | 69 | def __repr__(self): 70 | return '{}({})'.format(type(self).__name__, ', '.join('{}={!r}'.format(k, getattr(self, k)) for k in self.descriptor.annotations.keys())) 71 | 72 | def pack(self): 73 | data = tuple(getattr(self, fld) for fld in self.descriptor.annotations) 74 | return struct.pack(self.descriptor.fmt, *data) 75 | 76 | @classmethod 77 | def calcsize(cls): 78 | return cls.descriptor.size 79 | 80 | @classmethod 81 | def unpack(cls, buffer): 82 | data = struct.unpack(cls.descriptor.fmt, bytes(buffer)) 83 | 84 | r = cls() 85 | for k, v in zip(cls.descriptor.annotations, data): 86 | setattr(r, k, v) 87 | return r 88 | 89 | @classmethod 90 | def unpack_from(cls, buffer, offset = 0): 91 | desc = cls.descriptor 92 | if isinstance(buffer, bytes): 93 | data = struct.unpack_from(desc.fmt, buffer, offset) 94 | else: 95 | data = struct.unpack(desc.fmt, bytes(buffer[offset:offset+desc.size])) 96 | 97 | r = cls() 98 | for k, v in zip(desc.annotations, data): 99 | setattr(r, k, v) 100 | return r 101 | 102 | @classmethod 103 | def unpack_from_io(cls, fileobj): 104 | return cls.unpack_from(fileobj.read(cls.calcsize())) 105 | 106 | u8 = Annotation('B', 0) 107 | u16 = Annotation('H', 0) 108 | u32 = Annotation('I', 0) 109 | u64 = Annotation('Q', 0) 110 | i8 = Annotation('b', 0) 111 | i16 = Annotation('h', 0) 112 | i32 = Annotation('i', 0) 113 | i64 = Annotation('q', 0) 114 | char = Annotation('s', '\0') 115 | -------------------------------------------------------------------------------- /pe_tools/pdb.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | from .pe_parser import _IMAGE_SECTION_HEADER 3 | from . import cvinfo as cv 4 | from grope import rope, BlobIO 5 | from .struct3 import Struct3, char, u32, i32, u16 6 | import struct 7 | 8 | pdb_signature = b'Microsoft C/C++ MSF 7.00\r\n\x1aDS\0\0\0' 9 | 10 | class PdbFileHeader(Struct3): 11 | magic: char[len(pdb_signature)] 12 | block_size: u32 13 | free_block: u32 14 | block_count: u32 15 | directory_size: u32 16 | _reserved: u32 17 | 18 | class DbiStreamHeader(Struct3): 19 | VersionSignature: u32 20 | VersionHeader: u32 21 | Age: u32 22 | GlobalStreamIndex: u16 23 | BuildNumber: u16 24 | PublicStreamIndex: u16 25 | PdbDllVersion: u16 26 | SymRecordStream: u16 27 | PdbDllRbld: u16 28 | ModInfoSize: i32 29 | SectionContributionSize: i32 30 | SectionMapSize: i32 31 | SourceInfoSize: i32 32 | TypeServerMapSize: i32 33 | MFCTypeServerIndex: u32 34 | OptionalDbgHeaderSize: i32 35 | ECSubstreamSize: i32 36 | Flags: u16 37 | Machine: u16 38 | Padding: u32 39 | 40 | class SectionContribEntry2(Struct3): 41 | Section: u16 42 | Padding1: char[2] 43 | Offset: i32 44 | Size: i32 45 | Characteristics: u32 46 | ModuleIndex: u16 47 | Padding2: char[2] 48 | DataCrc: u32 49 | RelocCrc: u32 50 | ISectCoff: u32 51 | 52 | class SectionMapHeader(Struct3): 53 | Count: u16 54 | LogCount: u16 55 | 56 | class SectionMapEntry(Struct3): 57 | Flags: u16 58 | Ovl: u16 59 | Group: u16 60 | Frame: u16 61 | SectionName: u16 62 | ClassName: u16 63 | Offset: u32 64 | SectionLength: u32 65 | 66 | class PublicSymbol(NamedTuple): 67 | name: str 68 | segment: int 69 | offset: int 70 | rva: int 71 | 72 | class PdbFile: 73 | def __init__(self, blob, block_size, streams): 74 | self._blob = blob 75 | self._block_size = block_size 76 | self._streams = streams 77 | self._stream_bytes = [None]*len(streams) 78 | 79 | self._dbihdr = None 80 | self._sections = None 81 | self._addr_map = None 82 | 83 | def _parse_dbi(self): 84 | if self._dbihdr is not None: 85 | return 86 | 87 | dbi = self.get_stream(3) 88 | hdr = DbiStreamHeader.unpack_from(dbi) 89 | 90 | offs = hdr.size + hdr.ModInfoSize + hdr.SectionContributionSize + hdr.SectionMapSize + hdr.SourceInfoSize + hdr.TypeServerMapSize + hdr.ECSubstreamSize 91 | 92 | cnt = hdr.OptionalDbgHeaderSize // 2 93 | debug_streams = struct.unpack_from(f'<{cnt}h', dbi, offs) 94 | 95 | secstream = self.get_stream(debug_streams[5]) 96 | offs = 0 97 | 98 | sections = [] 99 | while offs < len(secstream): 100 | sections.append(_IMAGE_SECTION_HEADER.unpack_from(secstream, offs)) 101 | offs += _IMAGE_SECTION_HEADER.size 102 | self._sections = sections 103 | 104 | addr_map = [] 105 | 106 | offs = hdr.size + hdr.ModInfoSize + hdr.SectionContributionSize 107 | maphdr = SectionMapHeader.unpack_from(dbi, offs) 108 | offs += maphdr.size 109 | for _ in range(maphdr.Count): 110 | entry = SectionMapEntry.unpack_from(dbi, offs) 111 | offs += entry.size 112 | 113 | if entry.Frame: 114 | addr_map.append(sections[entry.Frame - 1].VirtualAddress) 115 | else: 116 | addr_map.append(0) 117 | 118 | self._addr_map = addr_map 119 | self._dbihdr = hdr 120 | 121 | def machine_type(self): 122 | self._parse_dbi() 123 | return self._dbihdr.Machine 124 | 125 | def get_public_symbols(self): 126 | self._parse_dbi() 127 | hdr = self._dbihdr 128 | 129 | syms = self.get_stream(hdr.SymRecordStream) 130 | 131 | offs = 0 132 | while offs < len(syms): 133 | reclen, rectp = struct.unpack_from(' len(blob): 130 | raise RuntimeError('resource is outside the resource blob') 131 | 132 | return blob[entry.DataRva - base:entry.DataRva + entry.Size - base] 133 | 134 | def parse_tree(offs): 135 | r = {} 136 | 137 | fin = BlobIO(blob[offs:]) 138 | 139 | node = _RESOURCE_DIRECTORY_TABLE.unpack_from_io(fin) 140 | name_entries = [_RESOURCE_DIRECTORY_ENTRY.unpack_from_io(fin) for i in range(node.NumberOfNameEntries)] 141 | id_entries = [_RESOURCE_DIRECTORY_ENTRY.unpack_from_io(fin) for i in range(node.NumberOfIdEntries)] 142 | 143 | for entry in name_entries: 144 | name = parse_string(entry.NameOrId & ~(1<<31)) 145 | if entry.Offset & (1<<31): 146 | r[name] = parse_tree(entry.Offset & ~(1<<31)) 147 | else: 148 | r[name] = parse_data(entry.Offset) 149 | 150 | for entry in id_entries: 151 | if entry.Offset & (1<<31): 152 | r[entry.NameOrId] = parse_tree(entry.Offset & ~(1<<31)) 153 | else: 154 | r[entry.NameOrId] = parse_data(entry.Offset) 155 | 156 | return r 157 | 158 | return parse_tree(0) 159 | 160 | class _PrepackedResources: 161 | def __init__(self, entries, strings, blobs): 162 | self._entries = entries 163 | self._strings = strings 164 | self._blobs = blobs 165 | 166 | self.size = sum(ent.size for ent in self._entries) + len(strings) + len(blobs) 167 | 168 | def pack(self, base): 169 | def _transform(ent): 170 | if not isinstance(ent, _RESOURCE_DATA_ENTRY): 171 | return ent 172 | return _RESOURCE_DATA_ENTRY(ent, DataRva=ent.DataRva + base) 173 | 174 | ents = [_transform(ent).pack() for ent in self._entries] 175 | return b''.join(ents) + self._strings + bytes(self._blobs) 176 | 177 | def _prepack(rsrc): 178 | if isinstance(rsrc, dict): 179 | name_keys = [key for key in rsrc.keys() if isinstance(key, str)] 180 | id_keys = [key for key in rsrc.keys() if not isinstance(key, str)] 181 | 182 | name_keys.sort() 183 | id_keys.sort() 184 | 185 | r = [] 186 | children = [] 187 | 188 | r.append(_RESOURCE_DIRECTORY_TABLE( 189 | Characteristics=0, 190 | Timestamp=0, 191 | Major=0, 192 | Minor=0, 193 | NumberOfNameEntries=len(name_keys), 194 | NumberOfIdEntries=len(id_keys), 195 | )) 196 | 197 | for keys in (name_keys, id_keys): 198 | for name in keys: 199 | items = _prepack(rsrc[name]) 200 | children.extend(items) 201 | r.append(_RESOURCE_DIRECTORY_ENTRY( 202 | NameOrId=name, 203 | Offset=items[0] 204 | )) 205 | 206 | r.extend(children) 207 | return r 208 | else: 209 | return [_RESOURCE_DATA_ENTRY( 210 | DataRva=rsrc, 211 | Size=len(rsrc), 212 | Codepage=0, 213 | Reserved=0 214 | )] 215 | 216 | def pe_resources_prepack(rsrc): 217 | entries = _prepack(rsrc) 218 | 219 | strings = [] 220 | string_map = {} 221 | def add_string(s): 222 | r = string_map.get(s) 223 | if r is None: 224 | encoded = s.encode('utf-16le') 225 | 226 | r = sum(len(ss) for ss in strings) 227 | string_map[s] = r 228 | 229 | strings.append(_STRING_HEADER(Length=len(encoded)//2).pack()) 230 | strings.append(encoded) 231 | return r 232 | 233 | _entry_offsets = {} 234 | offs = 0 235 | for ent in entries: 236 | _entry_offsets[ent] = offs 237 | offs += ent.size 238 | 239 | table_size = offs 240 | for ent in entries: 241 | if isinstance(ent, _RESOURCE_DIRECTORY_ENTRY): 242 | if isinstance(ent.NameOrId, str): 243 | ent.NameOrId = (1<<31) | (table_size + add_string(ent.NameOrId)) 244 | 245 | strings = b''.join(strings) 246 | aligned_strings_len = align16(len(strings)) 247 | strings += b'\0' * (aligned_strings_len - len(strings)) 248 | 249 | data_offs = table_size + len(strings) 250 | blobs = [] 251 | 252 | for ent in entries: 253 | if isinstance(ent, _RESOURCE_DIRECTORY_ENTRY): 254 | if isinstance(ent.Offset, _RESOURCE_DIRECTORY_TABLE): 255 | ent.Offset = (1<<31) | _entry_offsets[ent.Offset] 256 | else: 257 | ent.Offset = _entry_offsets[ent.Offset] 258 | elif isinstance(ent, _RESOURCE_DATA_ENTRY): 259 | blob = ent.DataRva 260 | ent.DataRva = data_offs 261 | 262 | blobs.append(blob) 263 | aligned_size = align8(len(blob)) 264 | pad = aligned_size - len(blob) 265 | if pad: 266 | blobs.append(b'\0' * pad) 267 | 268 | data_offs += aligned_size 269 | 270 | return _PrepackedResources(entries, strings, rope(*blobs)) 271 | -------------------------------------------------------------------------------- /pe_tools/version_info.py: -------------------------------------------------------------------------------- 1 | from .struct3 import Struct3, u16, u32 2 | from .utils import align4 3 | from grope import rope 4 | import struct 5 | 6 | class _VS_FIXEDFILEINFO(Struct3): 7 | dwSignature: u32 8 | dwStrucVersion: u32 9 | dwFileVersionMS: u32 10 | dwFileVersionLS: u32 11 | dwProductVersionMS: u32 12 | dwProductVersionLS: u32 13 | dwFileFlagsMask: u32 14 | dwFileFlags: u32 15 | dwFileOS: u32 16 | dwFileType: u32 17 | dwFileSubtype: u32 18 | dwFileDateMS: u32 19 | dwFileDateLS: u32 20 | 21 | @property 22 | def file_version_tuple(self): 23 | return ( 24 | self.dwFileVersionMS >> 16, 25 | self.dwFileVersionMS & 0xffff, 26 | self.dwFileVersionLS >> 16, 27 | self.dwFileVersionLS & 0xffff, 28 | ) 29 | 30 | @property 31 | def product_version_tuple(self): 32 | return ( 33 | self.dwProductVersionMS >> 16, 34 | self.dwProductVersionMS & 0xffff, 35 | self.dwProductVersionLS >> 16, 36 | self.dwProductVersionLS & 0xffff, 37 | ) 38 | 39 | @property 40 | def file_version(self): 41 | return '{}.{}.{}.{}'.format(*self.file_version_tuple) 42 | 43 | @property 44 | def product_version(self): 45 | return '{}.{}.{}.{}'.format(*self.product_version_tuple) 46 | 47 | def set_file_version(self, major, minor=0, patch=0, build=0): 48 | self.dwFileVersionMS = (major << 16) | minor 49 | self.dwFileVersionLS = (patch << 16) | build 50 | 51 | def set_product_version(self, major, minor=0, patch=0, build=0): 52 | self.dwProductVersionMS = (major << 16) | minor 53 | self.dwProductVersionLS = (patch << 16) | build 54 | 55 | FIXEDFILEINFO_SIG = 0xFEEF04BD 56 | 57 | class _NODE_HEADER(Struct3): 58 | wLength: u16 59 | wValueLength: u16 60 | wType: u16 61 | 62 | class _VerNode: 63 | def __init__(self, key, value, children): 64 | self.name = key 65 | self.value = value 66 | self.children = children 67 | 68 | class VersionInfo: 69 | def __init__(self, root=None): 70 | if root is None: 71 | root = _VerNode('', _VS_FIXEDFILEINFO().pack(), []) 72 | self._root = root 73 | 74 | def get(self, name, default=None): 75 | components = [c for c in name.split('/') if c] 76 | 77 | cur = self._root 78 | for c in components: 79 | for child in cur.children: 80 | if child.name == c: 81 | cur = child 82 | break 83 | else: 84 | return default 85 | 86 | return cur 87 | 88 | def get_fixed_info(self): 89 | fi = _VS_FIXEDFILEINFO.unpack(self._root.value) 90 | if fi.dwSignature != FIXEDFILEINFO_SIG: 91 | raise ValueError('FIXEDFILEINFO_SIG mismatch') 92 | return fi 93 | 94 | def set_fixed_info(self, fi): 95 | self._root.value = fi.pack() 96 | 97 | def string_file_info(self): 98 | r = {} 99 | sfi = self.get('StringFileInfo') 100 | if sfi is not None: 101 | for fi in sfi.children: 102 | if len(fi.name) != 8: 103 | raise RuntimeError('corrupted string file info') 104 | langid = int(fi.name[:4], 16) 105 | cp = int(fi.name[4:], 16) 106 | 107 | tran = {} 108 | for s in fi.children: 109 | tran[s.name] = s.value 110 | r[(langid, cp)] = tran 111 | return r 112 | 113 | def set_string_file_info(self, translations): 114 | children = [] 115 | trans = [] 116 | for (langid, cp), strs in translations.items(): 117 | tran_children = [_VerNode(k, v, []) for k, v in sorted(strs.items())] 118 | children.append(_VerNode('{:04x}{:04x}'.format(langid, cp), None, tran_children)) 119 | trans.append(struct.pack(' 4 or any(part < 0 or part >= 2**16 for part in self._parts): 15 | raise ValueError('invalid version') 16 | 17 | while len(self._parts) < 4: 18 | self._parts.append(0) 19 | 20 | def get_ms_ls(self): 21 | ms = (self._parts[0] << 16) + self._parts[1] 22 | ls = (self._parts[2] << 16) + self._parts[3] 23 | return ms, ls 24 | 25 | def format(self): 26 | return ', '.join(str(part) for part in self._parts) 27 | 28 | class _IdentityReplace: 29 | def __init__(self, val): 30 | self._val = val 31 | 32 | def __call__(self, s): 33 | return self._val 34 | 35 | class _ReReplace: 36 | def __init__(self, compiled_re, sub): 37 | self._compiled_re = compiled_re 38 | self._sub = sub 39 | 40 | def __call__(self, s): 41 | return self._compiled_re.sub(self._sub, s) 42 | 43 | RT_VERSION = KnownResourceTypes.RT_VERSION 44 | RT_MANIFEST = KnownResourceTypes.RT_MANIFEST 45 | 46 | def main(): 47 | ap = argparse.ArgumentParser(fromfile_prefix_chars='@', description="Parses and edits resources in Windows executable (PE) files.") 48 | ap.add_argument('--remove-signature', action='store_true', help="remove the signature. If the file contains one, editing the file will fail") 49 | ap.add_argument('--ignore-trailer', action='store_true', help="keep trailing data (typically in a setup program) intact, move them if necessary") 50 | ap.add_argument('--remove-trailer', action='store_true', help="remove any trailing data from the output") 51 | ap.add_argument('--update-checksum', action='store_true', help="set the correct checksum (can be slow on large files), zero it out otherwise") 52 | ap.add_argument('--clear', '-C', action='store_true', help="remove existing resources, except for the manifest") 53 | ap.add_argument('--clear-manifest', action='store_true', help="remove the manifest resource") 54 | 55 | gp = ap.add_argument_group('informational (applied before any edits)') 56 | gp.add_argument('--print-tree', '-t', action='store_true', help="prints the outline of the resource tree") 57 | gp.add_argument('--print-version', '-v', action='store_true', help="prints all version info structures") 58 | 59 | gp = ap.add_argument_group('editor commands (can be used multiple times)') 60 | gp.add_argument('--apply', '-A', action='append', metavar="RES", default=[], help="apply a custom .res file, overwrite any matching resource entries") 61 | gp.add_argument('--add-dependency', '-M', action='append', metavar="DEP", default=[], help="add dependency. DEP should be a space separated list of key=value pairs, e.g. " + 62 | "\"type=win32 name=Microsoft.Windows.Common-Controls version=6.0.0.0 processorArchitecture=* publicKeyToken=6595b64144ccf1df language=*\"") 63 | gp.add_argument('--set-version', '-V', action='append', metavar="STR", help="updates the specified version-info field, e.g. FileVersion=\"1, 2, 3, 4\"") 64 | gp.add_argument('--set-resource', '-R', metavar=('TYPE', 'NAME', 'LANG', 'FILE'), nargs=4, action='append', default=[], help='set a resource entry to the contents of a file, e.g. "-R RT_RCDATA prog.exe 0 prog.exe"') 65 | 66 | ap.add_argument('--output', '-o', help="write the edited contents to OUTPUT instead of editing the input file in-place") 67 | ap.add_argument('file', help="the PE file to parse and edit") 68 | 69 | if not sys.argv[1:]: 70 | ap.print_help() 71 | return 0 72 | 73 | args = ap.parse_args() 74 | 75 | fin = open(args.file, 'rb') 76 | pe = parse_pe(grope.wrap_io(fin)) 77 | resources = pe.parse_resources() 78 | if args.print_tree: 79 | if resources is None: 80 | print('no resources in the PE file') 81 | else: 82 | print('resources:') 83 | for resource_type in resources: 84 | print(' {}'.format(KnownResourceTypes.get_type_name(resource_type))) 85 | for name in resources[resource_type]: 86 | print(' {}'.format(name)) 87 | for lang in resources[resource_type][name]: 88 | print(' {}: size={}'.format(lang, len(resources[resource_type][name][lang]))) 89 | 90 | if resources is None: 91 | resources = {} 92 | 93 | if args.print_version: 94 | for name in resources[RT_VERSION]: 95 | for lang in resources[RT_VERSION][name]: 96 | print('version info: {} {}'.format(name, lang)) 97 | 98 | vi = parse_version_info(resources[RT_VERSION][name][lang]) 99 | fixed = vi.get_fixed_info() 100 | 101 | print(' file version: {}'.format(fixed.file_version)) 102 | print(' product version: {}'.format(fixed.product_version)) 103 | for k in fixed.descriptor.names: 104 | print(' {}: 0x{:x}'.format(k, getattr(fixed, k))) 105 | 106 | if not args.clear and not args.apply and not args.add_dependency and not args.set_version and not args.set_resource: 107 | return 0 108 | 109 | if pe.has_trailer(): 110 | if not args.ignore_trailer and not args.remove_trailer: 111 | print('error: the file contains trailing data, ignore with --ignore-trailer', file=sys.stderr) 112 | return 1 113 | 114 | if args.remove_trailer: 115 | pe.remove_trailer() 116 | 117 | if pe.has_signature(): 118 | if not args.remove_signature and not args.remove_trailer: 119 | print('error: the file contains a signature', file=sys.stderr) 120 | return 1 121 | 122 | pe.remove_signature() 123 | 124 | if not pe.is_dir_safely_resizable(IMAGE_DIRECTORY_ENTRY_RESOURCE): 125 | print('error: the resource section is not resizable: {}'.format(args.file), file=sys.stderr) 126 | return 3 127 | 128 | if args.clear: 129 | resources = { k: v for k, v in resources.items() if k == RT_MANIFEST } 130 | 131 | if args.clear_manifest: 132 | if RT_MANIFEST in resources: 133 | del resources[RT_MANIFEST] 134 | 135 | for res_file in args.apply: 136 | res_fin = open(res_file, 'rb') 137 | # must not close res_fin until the ropes are gone 138 | 139 | r = parse_prelink_resources(grope.wrap_io(res_fin)) 140 | for resource_type in r: 141 | for name in r[resource_type]: 142 | for lang in r[resource_type][name]: 143 | resources.setdefault(resource_type, {}).setdefault(name, {})[lang] = r[resource_type][name][lang] 144 | 145 | for rtype, rname, lang, inname in args.set_resource: 146 | res_fin = open(inname, 'rb') 147 | rtype = getattr(KnownResourceTypes, rtype, rtype) 148 | if rname.startswith('#'): 149 | rname = int(rname[1:], 10) 150 | else: 151 | rname = rname.upper() 152 | resources.setdefault(rtype, {}).setdefault(rname, {})[int(lang)] = grope.wrap_io(res_fin) 153 | 154 | if args.add_dependency: 155 | man_data = None 156 | for name in resources.get(RT_MANIFEST, ()): 157 | for lang in resources[name]: 158 | if man_data is not None: 159 | print('error: multiple manifest resources found', file=sys.stderr) 160 | return 4 161 | man_data = resources[name][lang] 162 | man_name = name 163 | man_lang = lang 164 | 165 | if man_data is None: 166 | man_doc = xml.dom.minidom.getDOMImplementation().createDocument(None, 'dependency', None) 167 | man = man_doc.documentElement 168 | else: 169 | man_doc = xml.dom.minidom.parseString(bytes(man_data)) 170 | man = man_doc.documentElement 171 | 172 | dependent_assembly = man_doc.getElementById('dependentAssembly') 173 | if not dependent_assembly: 174 | dependent_assembly = man_doc.createElement('dependentAssembly') 175 | man.append(dependent_assembly) 176 | 177 | for dep in args.add_dependency: 178 | dep_elem = man_doc.createElement('assemblyIdentity') 179 | for tok in dep.split(): 180 | k, v = tok.split('=', 1) 181 | dep_elem.attrib[k] = v 182 | dependent_assembly.appendChild(dep_elem) 183 | 184 | resources[RT_MANIFEST][man_name][man_lang] = b'\xef\xbb\xbf' + man_doc.toxml(encoding='utf-8') 185 | 186 | if args.set_version: 187 | ver_data = None 188 | for name in resources.get(RT_VERSION, ()): 189 | for lang in resources[RT_VERSION][name]: 190 | if ver_data is not None: 191 | print('error: multiple manifest resources found', file=sys.stderr) 192 | return 4 193 | ver_data = resources[RT_VERSION][name][lang] 194 | ver_name = name 195 | ver_lang = lang 196 | 197 | if ver_data is None: 198 | ver_data = VersionInfo() 199 | 200 | params = {} 201 | for param in args.set_version: 202 | toks = param.split('=', 1) 203 | if len(toks) != 2: 204 | print('error: version infos must be in the form "name=value"', file=sys.stderr) 205 | return 2 206 | name, value = toks 207 | 208 | if value.startswith('/') and value.endswith('/'): 209 | pattern, sub = value[1:-1].split('/', 1) 210 | r = re.compile(pattern) 211 | params[name] = _ReReplace(r, sub) 212 | else: 213 | params[name] = _IdentityReplace(value) 214 | 215 | vi = parse_version_info(ver_data) 216 | 217 | fvi = vi.get_fixed_info() 218 | if 'FileVersion' in params: 219 | ver = Version(params['FileVersion'](None)) 220 | fvi.dwFileVersionMS, fvi.dwFileVersionLS = ver.get_ms_ls() 221 | if 'ProductVersion' in params: 222 | ver = Version(params['ProductVersion'](None)) 223 | fvi.dwProductVersionMS, fvi.dwProductVersionLS = ver.get_ms_ls() 224 | vi.set_fixed_info(fvi) 225 | 226 | sfi = vi.string_file_info() 227 | for _, strings in sfi.items(): 228 | for k, fn in params.items(): 229 | val = fn(strings.get(k, '')) 230 | if val: 231 | strings[k] = val 232 | elif k in strings: 233 | del strings[k] 234 | vi.set_string_file_info(sfi) 235 | resources[RT_VERSION][ver_name][ver_lang] = vi.pack() 236 | 237 | prepacked = pe_resources_prepack(resources) 238 | addr = pe.resize_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE, prepacked.size) 239 | pe.set_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE, prepacked.pack(addr)) 240 | 241 | if not args.output: 242 | fout, fout_name = tempfile.mkstemp(dir=os.path.split(args.file)[0]) 243 | fout = os.fdopen(fout, mode='w+b') 244 | try: 245 | grope.dump(pe.to_blob(update_checksum=args.update_checksum), fout) 246 | 247 | fin.close() 248 | fout.close() 249 | except: 250 | fout.close() 251 | os.remove(fout_name) 252 | raise 253 | else: 254 | os.remove(args.file) 255 | os.rename(fout_name, args.file) 256 | 257 | else: 258 | with open(args.output, 'wb') as fout: 259 | grope.dump(pe.to_blob(), fout) 260 | 261 | return 0 262 | 263 | if __name__ == '__main__': 264 | main() 265 | -------------------------------------------------------------------------------- /pe_tools/pe_parser.py: -------------------------------------------------------------------------------- 1 | import struct, io 2 | from dataclasses import dataclass 3 | from grope import BlobIO, rope 4 | from .struct3 import Struct3, u8, u16, u32, u64, char 5 | from .rsrc import parse_pe_resources 6 | from .rsrc import KnownResourceTypes 7 | import uuid 8 | from .version_info import parse_version_info 9 | 10 | class _IMAGE_FILE_HEADER(Struct3): 11 | Machine: u16 12 | NumberOfSections: u16 13 | TimeDateStamp: u32 14 | PointerToSymbolTable: u32 15 | NumberOfSymbols: u32 16 | SizeOfOptionalHeader: u16 17 | Characteristics: u16 18 | 19 | IMAGE_NT_OPTIONAL_HDR32_MAGIC = 0x10b 20 | IMAGE_NT_OPTIONAL_HDR64_MAGIC = 0x20b 21 | 22 | class _IMAGE_OPTIONAL_HEADER32(Struct3): 23 | MajorLinkerVersion: u8 24 | MinorLinkerVersion: u8 25 | SizeOfCode: u32 26 | SizeOfInitializedData: u32 27 | SizeOfUninitializedData: u32 28 | AddressOfEntryPoint: u32 29 | BaseOfCode: u32 30 | BaseOfData: u32 31 | ImageBase: u32 32 | SectionAlignment: u32 33 | FileAlignment: u32 34 | MajorOperatingSystemVersion: u16 35 | MinorOperatingSystemVersion: u16 36 | MajorImageVersion: u16 37 | MinorImageVersion: u16 38 | MajorSubsystemVersion: u16 39 | MinorSubsystemVersion: u16 40 | Reserved1: u32 41 | SizeOfImage: u32 42 | SizeOfHeaders: u32 43 | CheckSum: u32 44 | Subsystem: u16 45 | DllCharacteristics: u16 46 | SizeOfStackReserve: u32 47 | SizeOfStackCommit: u32 48 | SizeOfHeapReserve: u32 49 | SizeOfHeapCommit: u32 50 | LoaderFlags: u32 51 | NumberOfRvaAndSizes: u32 52 | 53 | class _IMAGE_OPTIONAL_HEADER64(Struct3): 54 | MajorLinkerVersion: u8 55 | MinorLinkerVersion: u8 56 | SizeOfCode: u32 57 | SizeOfInitializedData: u32 58 | SizeOfUninitializedData: u32 59 | AddressOfEntryPoint: u32 60 | BaseOfCode: u32 61 | ImageBase: u64 62 | SectionAlignment: u32 63 | FileAlignment: u32 64 | MajorOperatingSystemVersion: u16 65 | MinorOperatingSystemVersion: u16 66 | MajorImageVersion: u16 67 | MinorImageVersion: u16 68 | MajorSubsystemVersion: u16 69 | MinorSubsystemVersion: u16 70 | Reserved1: u32 71 | SizeOfImage: u32 72 | SizeOfHeaders: u32 73 | CheckSum: u32 74 | Subsystem: u16 75 | DllCharacteristics: u16 76 | SizeOfStackReserve: u64 77 | SizeOfStackCommit: u64 78 | SizeOfHeapReserve: u64 79 | SizeOfHeapCommit: u64 80 | LoaderFlags: u32 81 | NumberOfRvaAndSizes: u32 82 | 83 | class _IMAGE_DATA_DIRECTORY(Struct3): 84 | VirtualAddress: u32 85 | Size: u32 86 | 87 | class _IMAGE_SECTION_HEADER(Struct3): 88 | Name: char[8] 89 | VirtualSize: u32 90 | VirtualAddress: u32 91 | SizeOfRawData: u32 92 | PointerToRawData: u32 93 | PointerToRelocations: u32 94 | PointerToLinenumbers: u32 95 | NumberOfRelocations: u16 96 | NumberOfLinenumbers: u16 97 | Characteristics: u32 98 | 99 | class _IMAGE_DEBUG_DIRECTORY(Struct3): 100 | Characteristics: u32 101 | TimeDateStamp: u32 102 | MajorVersion: u16 103 | MinorVersion: u16 104 | Type: u32 105 | SizeOfData: u32 106 | AddressOfRawData: u32 107 | PointerToRawData: u32 108 | 109 | class _IMAGE_DEBUG_CODEVIEW(Struct3): 110 | rsds: char[4] 111 | guid: char[16] 112 | age: u32 113 | 114 | @dataclass 115 | class PeIdent: 116 | image_name: str 117 | timestamp: int 118 | size_of_image: int 119 | 120 | @classmethod 121 | def from_pelink(cls, pelink: str): 122 | fname, tssize = pelink.split('/') 123 | ts = int(tssize[:8], 16) 124 | size = int(tssize[8:], 16) 125 | return cls(image_name=fname, timestamp=ts, size_of_image=size) 126 | 127 | @property 128 | def pelink(self): 129 | return f'{self.image_name}/{self.timestamp:08x}{self.size_of_image:x}' 130 | 131 | def __str__(self): 132 | return self.pelink 133 | @dataclass 134 | class CodeviewLink: 135 | guid: uuid.UUID 136 | age: int 137 | filename: str 138 | 139 | @classmethod 140 | def from_bxlink(cls, bxlink: str): 141 | fname, bxlink = bxlink.split('{', 1) 142 | guid, age = bxlink.split('}', 1) 143 | return cls(guid=uuid.UUID(guid), age=int(age, 10), filename=fname) 144 | 145 | @property 146 | def bxlink(self): 147 | return f'{self.filename}{{{self.guid}}}{self.age}' 148 | 149 | @property 150 | def short_filename(self): 151 | return self.filename.rsplit('\\', 1)[-1] 152 | 153 | def __str__(self): 154 | return self.bxlink 155 | 156 | IMAGE_DEBUG_TYPE_CODEVIEW = 2 157 | 158 | IMAGE_SCN_TYPE_REG = 0x00000000 159 | IMAGE_SCN_TYPE_DSECT = 0x00000001 160 | IMAGE_SCN_TYPE_NOLOAD = 0x00000002 161 | IMAGE_SCN_TYPE_GROUP = 0x00000004 162 | IMAGE_SCN_TYPE_NO_PAD = 0x00000008 163 | IMAGE_SCN_TYPE_COPY = 0x00000010 164 | IMAGE_SCN_CNT_CODE = 0x00000020 165 | IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 166 | IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080 167 | IMAGE_SCN_LNK_OTHER = 0x00000100 168 | IMAGE_SCN_LNK_INFO = 0x00000200 169 | IMAGE_SCN_TYPE_OVER = 0x00000400 170 | IMAGE_SCN_LNK_REMOVE = 0x00000800 171 | IMAGE_SCN_LNK_COMDAT = 0x00001000 172 | IMAGE_SCN_NO_DEFER_SPEC_EXC = 0x00004000 173 | IMAGE_SCN_GPREL = 0x00008000 174 | IMAGE_SCN_MEM_FARDATA = 0x00008000 175 | IMAGE_SCN_MEM_PURGEABLE = 0x00020000 176 | IMAGE_SCN_MEM_16BIT = 0x00020000 177 | IMAGE_SCN_MEM_LOCKED = 0x00040000 178 | IMAGE_SCN_MEM_PRELOAD = 0x00080000 179 | IMAGE_SCN_ALIGN_1BYTES = 0x00100000 180 | IMAGE_SCN_ALIGN_2BYTES = 0x00200000 181 | IMAGE_SCN_ALIGN_4BYTES = 0x00300000 182 | IMAGE_SCN_ALIGN_8BYTES = 0x00400000 183 | IMAGE_SCN_ALIGN_16BYTES = 0x00500000 184 | IMAGE_SCN_ALIGN_32BYTES = 0x00600000 185 | IMAGE_SCN_ALIGN_64BYTES = 0x00700000 186 | IMAGE_SCN_ALIGN_128BYTES = 0x00800000 187 | IMAGE_SCN_ALIGN_256BYTES = 0x00900000 188 | IMAGE_SCN_ALIGN_512BYTES = 0x00A00000 189 | IMAGE_SCN_ALIGN_1024BYTES = 0x00B00000 190 | IMAGE_SCN_ALIGN_2048BYTES = 0x00C00000 191 | IMAGE_SCN_ALIGN_4096BYTES = 0x00D00000 192 | IMAGE_SCN_ALIGN_8192BYTES = 0x00E00000 193 | IMAGE_SCN_ALIGN_MASK = 0x00F00000 194 | IMAGE_SCN_LNK_NRELOC_OVFL = 0x01000000 195 | IMAGE_SCN_MEM_DISCARDABLE = 0x02000000 196 | IMAGE_SCN_MEM_NOT_CACHED = 0x04000000 197 | IMAGE_SCN_MEM_NOT_PAGED = 0x08000000 198 | IMAGE_SCN_MEM_SHARED = 0x10000000 199 | IMAGE_SCN_MEM_EXECUTE = 0x20000000 200 | IMAGE_SCN_MEM_READ = 0x40000000 201 | IMAGE_SCN_MEM_WRITE = 0x80000000 202 | 203 | 204 | def _align(offs, alignment): 205 | return (offs + alignment - 1) // alignment * alignment 206 | 207 | IMAGE_DIRECTORY_ENTRY_EXPORT = 0 208 | IMAGE_DIRECTORY_ENTRY_IMPORT = 1 209 | IMAGE_DIRECTORY_ENTRY_RESOURCE = 2 210 | IMAGE_DIRECTORY_ENTRY_EXCEPTION = 3 211 | IMAGE_DIRECTORY_ENTRY_SECURITY = 4 212 | IMAGE_DIRECTORY_ENTRY_BASERELOC = 5 213 | IMAGE_DIRECTORY_ENTRY_DEBUG = 6 214 | IMAGE_DIRECTORY_ENTRY_ARCHITECTURE = 7 215 | IMAGE_DIRECTORY_ENTRY_GLOBALPTR = 8 216 | IMAGE_DIRECTORY_ENTRY_TLS = 9 217 | IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10 218 | IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT = 11 219 | IMAGE_DIRECTORY_ENTRY_IAT = 12 220 | IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT = 13 221 | IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR = 14 222 | 223 | class _PeSection: 224 | def __init__(self, hdr, data): 225 | self.hdr = hdr 226 | self.data = data 227 | 228 | def pe_checksum(blob): 229 | total_len = len(blob) 230 | 231 | r = 0 232 | while len(blob) >= 0x1000: 233 | words = struct.unpack('<2048H', bytes(blob[:0x1000])) 234 | r += sum(words) 235 | blob = blob[0x1000:] 236 | 237 | if len(blob) % 2 != 0: 238 | blob = rope(blob, b'\0') 239 | words = struct.unpack('<' + 'H'*(len(blob) // 2), bytes(blob)) 240 | r += sum(words) 241 | 242 | while r > 0xffff: 243 | c = r 244 | r = 0 245 | while c: 246 | r += c & 0xffff 247 | c >>= 16 248 | 249 | return r + total_len 250 | 251 | def _read(blob, fmt): 252 | size = struct.calcsize(fmt) 253 | return struct.unpack(fmt, bytes(blob[:size])) 254 | 255 | def parse_rsds_blob(blob): 256 | if len(blob) < _IMAGE_DEBUG_CODEVIEW.size: 257 | return None 258 | 259 | cv = _IMAGE_DEBUG_CODEVIEW.unpack_from(blob) 260 | if cv.rsds != b'RSDS': 261 | return None 262 | 263 | fname = bytes(blob[_IMAGE_DEBUG_CODEVIEW.size:]) 264 | fname, *_ = fname.split(b'\0', 1) 265 | return CodeviewLink(uuid.UUID(bytes_le=cv.guid), cv.age, fname.decode('utf-8')) 266 | 267 | class _PeFile: 268 | def __init__(self, blob, verify_checksum=False): 269 | pe_offs, = _read(blob[0x3c:], '= stop: 371 | sec_offs = start - sec.hdr.VirtualAddress 372 | init_size = min(sec.hdr.SizeOfRawData - sec_offs, stop - start) 373 | uninit_size = stop - start - init_size 374 | if len(sec.data) < sec_offs + init_size: 375 | raise RuntimeError('PE file corrupt: missing section content') 376 | return rope(sec.data[sec_offs:sec_offs + init_size], b'\0'*uninit_size) 377 | 378 | def has_trailer(self): 379 | return bool(self._trailer) 380 | 381 | def remove_trailer(self): 382 | self.remove_signature() 383 | self._trailer = b'' 384 | 385 | def has_signature(self): 386 | return len(self._data_directories) > IMAGE_DIRECTORY_ENTRY_SECURITY and self._data_directories[IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress != 0 387 | 388 | def remove_signature(self): 389 | if len(self._data_directories) < IMAGE_DIRECTORY_ENTRY_SECURITY: 390 | return 391 | 392 | dd = self._data_directories[IMAGE_DIRECTORY_ENTRY_SECURITY] 393 | if dd.Size == 0: 394 | return 395 | 396 | end_of_image = max(sec.hdr.PointerToRawData + sec.hdr.SizeOfRawData for sec in self._sections if sec.hdr.SizeOfRawData != 0) 397 | 398 | if dd.VirtualAddress + dd.Size != end_of_image + len(self._trailer): 399 | raise RuntimeError('signature is not at the end of the file') 400 | 401 | if dd.VirtualAddress < end_of_image: 402 | raise RuntimeError('signature is not contained in the pe trailer') 403 | 404 | self._trailer = self._trailer[:-dd.Size] 405 | dd.VirtualAddress = 0 406 | dd.Size = 0 407 | 408 | def has_directory(self, idx): 409 | if len(self._data_directories) < idx: 410 | return False 411 | 412 | dd = self._data_directories[idx] 413 | return dd.VirtualAddress != 0 414 | 415 | def find_directory(self, idx): 416 | if len(self._data_directories) < idx: 417 | return None 418 | 419 | dd = self._data_directories[idx] 420 | if dd.VirtualAddress == 0: 421 | return None 422 | 423 | return slice(dd.VirtualAddress, dd.VirtualAddress + dd.Size) 424 | 425 | def get_codeview_link(self): 426 | debug_dir = self.get_directory_contents(IMAGE_DIRECTORY_ENTRY_DEBUG) 427 | 428 | while debug_dir: 429 | dd = _IMAGE_DEBUG_DIRECTORY.unpack_from(debug_dir) 430 | debug_dir = debug_dir[_IMAGE_DEBUG_DIRECTORY.size:] 431 | 432 | if dd.Type == IMAGE_DEBUG_TYPE_CODEVIEW: 433 | dl = self._blob[dd.PointerToRawData:dd.PointerToRawData+dd.SizeOfData] 434 | 435 | cv = parse_rsds_blob(dl) 436 | if cv is not None: 437 | return cv 438 | return None 439 | 440 | def get_directory_contents(self, idx): 441 | dd = self.find_directory(idx) 442 | if dd is None: 443 | return None 444 | 445 | return self.get_vm(dd.start, dd.stop) 446 | 447 | def parse_resources(self): 448 | vm_slice = self.find_directory(IMAGE_DIRECTORY_ENTRY_RESOURCE) 449 | if vm_slice is None: 450 | return None 451 | 452 | data = self.get_vm(vm_slice.start, vm_slice.stop) 453 | return parse_pe_resources(data, vm_slice.start) 454 | 455 | def _get_version_info_dict(self): 456 | res = self.parse_resources() 457 | if not res: 458 | return 459 | return res.get(KnownResourceTypes.RT_VERSION, {}).get(1, {}) 460 | 461 | def get_version_info(self, langs=(0x0409, 0)): 462 | vis = self._get_version_info_dict() 463 | if not vis: 464 | return None 465 | 466 | for lngid in langs: 467 | vi = vis.get(lngid) 468 | if vi is not None: 469 | break 470 | else: 471 | lngid, vi = vis.popitem() 472 | 473 | return parse_version_info(vi) 474 | 475 | def get_file_version(self): 476 | vi = self.get_version_info() 477 | return vi.get_fixed_info().file_version_tuple if vi else None 478 | 479 | def get_product_version(self): 480 | vi = self.get_version_info() 481 | return vi.get_fixed_info().product_version_tuple if vi else None 482 | 483 | def _get_directory_section(self, dd_idx): 484 | if dd_idx >= len(self._data_directories): 485 | return None 486 | 487 | dd = self._data_directories[dd_idx] 488 | if dd.Size == 0: 489 | return None 490 | 491 | for sec_idx, sec in enumerate(self._sections): 492 | if sec.hdr.VirtualAddress == dd.VirtualAddress and sec.hdr.VirtualSize == dd.Size: 493 | return sec_idx 494 | 495 | def _find_vm_hole(self, secs, size): 496 | sorted_secs = sorted(secs, key=lambda sec: sec.hdr.VirtualAddress) 497 | i = 1 498 | while i < len(sorted_secs): 499 | start = self._mem_align(sorted_secs[i-1].hdr.VirtualAddress + sorted_secs[i-1].hdr.VirtualSize) 500 | stop = sorted_secs[i].hdr.VirtualAddress 501 | 502 | if stop - start >= size: 503 | return slice(start, self._mem_align(start + size)) 504 | 505 | i += 1 506 | 507 | start = self._mem_align(sorted_secs[-1].hdr.VirtualAddress + sorted_secs[-1].hdr.VirtualSize) 508 | return slice(start, self._mem_align(start + size)) 509 | 510 | def _resize_directory(self, idx, size): 511 | sec_idx = self._get_directory_section(idx) 512 | if sec_idx is None: 513 | raise RuntimeError('can\'t modify a directory that is not associated with a section') 514 | 515 | sec = self._sections[sec_idx] 516 | move_map = {} 517 | 518 | addr = self._mem_align(sec.hdr.VirtualAddress + size) 519 | for other_sec in self._sections[sec_idx + 1:]: 520 | move_map[other_sec] = addr 521 | addr = self._mem_align(addr + other_sec.hdr.VirtualSize) 522 | 523 | for dd in self._data_directories: 524 | if dd.VirtualAddress == 0: 525 | continue 526 | 527 | for osec, target_addr in move_map.items(): 528 | if osec.hdr.VirtualAddress <= dd.VirtualAddress <= osec.hdr.VirtualAddress + osec.hdr.VirtualSize: 529 | dd.VirtualAddress += target_addr - osec.hdr.VirtualAddress 530 | break 531 | 532 | for osec, target_addr in move_map.items(): 533 | osec.hdr.VirtualAddress = target_addr 534 | 535 | sec.hdr.VirtualSize = size 536 | 537 | dd = self._data_directories[idx] 538 | dd.Size = size 539 | 540 | return sec_idx, sec.hdr.VirtualAddress 541 | 542 | def is_dir_safely_resizable(self, idx): 543 | sec_idx = self._get_directory_section(idx) 544 | if sec_idx is None: 545 | return False 546 | 547 | return all((sec.hdr.Characteristics & IMAGE_SCN_MEM_DISCARDABLE) != 0 for sec in self._sections[sec_idx+1:]) 548 | 549 | def resize_directory(self, idx, size): 550 | _, addr = self._resize_directory(idx, size) 551 | return addr 552 | 553 | def set_directory(self, idx, blob): 554 | sec_idx, _ = self._resize_directory(idx, len(blob)) 555 | 556 | sec = self._sections[sec_idx] 557 | sec.data = blob 558 | 559 | def to_blob(self, update_checksum=False): 560 | self._opt_header.CheckSum = 0 561 | self._opt_header.SizeOfImage = max(self._mem_align(sec.hdr.VirtualAddress + sec.hdr.VirtualSize) for sec in self._sections) 562 | 563 | self._check_vm_overlaps() 564 | 565 | header_end = (len(self._dos_stub) + 4 + self._file_header.size + 2 + self._opt_header.size 566 | + len(self._data_directories) * _IMAGE_DATA_DIRECTORY.size + len(self._sections) * _IMAGE_SECTION_HEADER.size) 567 | section_offset = self._file_align(header_end) 568 | header_pad = section_offset - header_end 569 | 570 | for sec in self._sections: 571 | if sec.hdr.PointerToRawData == 0: 572 | continue 573 | sec.hdr.PointerToRawData = section_offset 574 | sec.hdr.SizeOfRawData = self._file_align(len(sec.data)) 575 | section_offset = section_offset + sec.hdr.SizeOfRawData 576 | 577 | new_file = [] 578 | 579 | new_file.append(self._dos_stub) 580 | new_file.append(b'PE\0\0') 581 | new_file.append(self._file_header.pack()) 582 | new_file.append(struct.pack('