mirror of
https://github.com/vivisect/vivisect
synced 2026-06-08 18:04:23 +00:00
1782 lines
64 KiB
Python
1782 lines
64 KiB
Python
import io
|
|
import os
|
|
import math
|
|
import struct
|
|
import logging
|
|
import binascii
|
|
|
|
|
|
import vstruct
|
|
import vstruct.defs.pe as vs_pe
|
|
import PE.clr as clr
|
|
|
|
import vivisect.exc as v_exc
|
|
|
|
from . import ordlookup
|
|
|
|
logger = logging.getLogger('vivisect')
|
|
|
|
PE32_MAGIC = 0x10b
|
|
PE32PLUS_MAGIC = 0x20b
|
|
|
|
IMAGE_FILE_RELOCS_STRIPPED = 0x0001
|
|
IMAGE_FILE_EXECUTABLE_IMAGE = 0x0002
|
|
IMAGE_FILE_LINE_NUMS_STRIPPED = 0x0004
|
|
IMAGE_FILE_LOCAL_SYMS_STRIPED = 0x0008
|
|
IMAGE_FILE_AGGRESSIVE_WS_TRIM = 0x00010
|
|
IMAGE_FILE_LARGE_ADDRESS_AWARE = 0x00020
|
|
# 0x0040 is reserved for future use
|
|
IMAGE_FILE_BYTES_REVERSED_LO = 0x0080
|
|
IMAGE_FILE_32BIT_MACHINE = 0x0100
|
|
IMAGE_FILE_DEBUG_STRIPPED = 0x0200
|
|
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP = 0x0400
|
|
IMAGE_FILE_NET_RUN_FROM_SWAP = 0x0800
|
|
IMAGE_FILE_SYSTEM = 0x1000
|
|
IMAGE_FILE_DLL = 0x2000
|
|
IMAGE_FILE_UP_SYSTEM_ONLY = 0x4000
|
|
IMAGE_FILE_REVERSED_HI = 0x8000
|
|
|
|
IMAGE_DLLCHARACTERISTICS_RESERVED_1 = 1
|
|
IMAGE_DLLCHARACTERISTICS_RESERVED_2 = 2
|
|
IMAGE_DLLCHARACTERISTICS_RESERVED_4 = 4
|
|
IMAGE_DLLCHARACTERISTICS_RESERVED_8 = 8
|
|
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE = 0x0040 # The DLL can be relocated at load time.
|
|
IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY = 0x0080 # Code integrity checks are forced. If you set this flag and a section contains only uninitialized data, set the PointerToRawData member of IMAGE_SECTION_HEADER for that section to zero; otherwise, the image will fail to load because the digital signature cannot be verified.
|
|
IMAGE_DLLCHARACTERISTICS_NX_COMPAT = 0x0100 # The image is compatible with data execution prevention (DEP).
|
|
IMAGE_DLLCHARACTERISTICS_NO_ISOLATION = 0x0200 # The image is isolation aware, but should not be isolated.
|
|
IMAGE_DLLCHARACTERISTICS_NO_SEH = 0x0400 # The image does not use structured exception handling (SEH). No handlers can be called in this image.
|
|
IMAGE_DLLCHARACTERISTICS_NO_BIND = 0x0800 # Do not bind the image.
|
|
IMAGE_DLLCHARACTERISTICS_RESERVED_1000 = 0x1000 # Reserved
|
|
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER = 0x2000 # A WDM driver.
|
|
IMAGE_DLLCHARACTERISTICS_RESERVED_4000 = 0x4000 # Reserved
|
|
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE = 0x8000
|
|
|
|
IMAGE_SUBSYSTEM_UNKNOWN = 0 #Unknown subsystem.
|
|
IMAGE_SUBSYSTEM_NATIVE = 1 #No subsystem required (device drivers and native system processes).
|
|
IMAGE_SUBSYSTEM_WINDOWS_GUI = 2 #Windows graphical user interface (GUI) subsystem.
|
|
IMAGE_SUBSYSTEM_WINDOWS_CUI = 3 #Windows character-mode user interface (CUI) subsystem.
|
|
IMAGE_SUBSYSTEM_OS2_CUI = 5 #OS/2 CUI subsystem.
|
|
IMAGE_SUBSYSTEM_POSIX_CUI = 7 #POSIX CUI subsystem.
|
|
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI = 9 #Windows CE system.
|
|
IMAGE_SUBSYSTEM_EFI_APPLICATION = 10 #Extensible Firmware Interface (EFI) application.
|
|
IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER = 11 #EFI driver with boot services.
|
|
IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER = 12 #EFI driver with run-time services.
|
|
IMAGE_SUBSYSTEM_EFI_ROM = 13 #EFI ROM image.
|
|
IMAGE_SUBSYSTEM_XBOX = 14 #Xbox system.
|
|
IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION = 16 #Boot application.
|
|
|
|
IMAGE_FILE_MACHINE_I386 = 0x014c
|
|
IMAGE_FILE_MACHINE_IA64 = 0x0200
|
|
IMAGE_FILE_MACHINE_AMD64 = 0x8664
|
|
|
|
IMAGE_FILE_MACHINE_ARM = 0x1c0
|
|
IMAGE_FILE_MACHINE_ARM64 = 0xaa64
|
|
IMAGE_FILE_MACHINE_ARMNT = 0x1c4 # ARMv7 or higher thumb mode only
|
|
IMAGE_FILE_MACHINE_THUMB = 0x1c2 # interworking arm/thumb
|
|
|
|
machine_names = {
|
|
IMAGE_FILE_MACHINE_I386: 'i386',
|
|
IMAGE_FILE_MACHINE_IA64: 'ia64',
|
|
IMAGE_FILE_MACHINE_AMD64: 'amd64',
|
|
|
|
IMAGE_FILE_MACHINE_ARM: 'arm',
|
|
IMAGE_FILE_MACHINE_ARM64: 'arm64',
|
|
IMAGE_FILE_MACHINE_ARMNT: 'thumb',
|
|
IMAGE_FILE_MACHINE_THUMB: 'thumb16',
|
|
}
|
|
|
|
IMAGE_REL_BASED_ABSOLUTE = 0
|
|
IMAGE_REL_BASED_HIGH = 1
|
|
IMAGE_REL_BASED_LOW = 2
|
|
IMAGE_REL_BASED_HIGHLOW = 3
|
|
IMAGE_REL_BASED_HIGHADJ = 4
|
|
IMAGE_REL_BASED_MIPS_JMPADDR = 5
|
|
IMAGE_REL_BASED_IA64_IMM64 = 9
|
|
IMAGE_REL_BASED_DIR64 = 10
|
|
|
|
IMAGE_DIRECTORY_ENTRY_EXPORT = 0 # Export Directory
|
|
IMAGE_DIRECTORY_ENTRY_IMPORT = 1 # Import Directory
|
|
IMAGE_DIRECTORY_ENTRY_RESOURCE = 2 # Resource Directory
|
|
IMAGE_DIRECTORY_ENTRY_EXCEPTION = 3 # Exception Directory
|
|
IMAGE_DIRECTORY_ENTRY_SECURITY = 4 # Security Directory
|
|
IMAGE_DIRECTORY_ENTRY_BASERELOC = 5 # Base Relocation Table
|
|
IMAGE_DIRECTORY_ENTRY_DEBUG = 6 # Debug Directory
|
|
IMAGE_DIRECTORY_ENTRY_COPYRIGHT = 7 # (X86 usage)
|
|
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE = 7 # Architecture Specific Data
|
|
IMAGE_DIRECTORY_ENTRY_GLOBALPTR = 8 # RVA of GP
|
|
IMAGE_DIRECTORY_ENTRY_TLS = 9 # TLS Directory
|
|
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10 # Load Configuration Directory
|
|
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT = 11 # Bound Import Directory in headers
|
|
IMAGE_DIRECTORY_ENTRY_IAT = 12 # Import Address Table
|
|
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT = 13 # Delay Load Import Descriptors
|
|
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR = 14 # COM Runtime descriptor
|
|
|
|
IMAGE_DEBUG_TYPE_UNKNOWN = 0
|
|
IMAGE_DEBUG_TYPE_COFF = 1
|
|
IMAGE_DEBUG_TYPE_CODEVIEW = 2
|
|
IMAGE_DEBUG_TYPE_FPO = 3
|
|
IMAGE_DEBUG_TYPE_MISC = 4
|
|
IMAGE_DEBUG_TYPE_EXCEPTION = 5
|
|
IMAGE_DEBUG_TYPE_FIXUP = 6
|
|
IMAGE_DEBUG_TYPE_OMAP_TO_SRC = 7
|
|
IMAGE_DEBUG_TYPE_OMAP_FROM_SRC = 8
|
|
IMAGE_DEBUG_TYPE_BORLAND = 9
|
|
IMAGE_DEBUG_TYPE_RESERVED10 = 10
|
|
IMAGE_DEBUG_TYPE_CLSID = 11
|
|
|
|
IMAGE_SCN_CNT_CODE = 0x00000020
|
|
IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040
|
|
IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080
|
|
IMAGE_SCN_LNK_OTHER = 0x00000100
|
|
IMAGE_SCN_LNK_INFO = 0x00000200
|
|
IMAGE_SCN_LNK_REMOVE = 0x00000800
|
|
IMAGE_SCN_LNK_COMDAT = 0x00001000
|
|
IMAGE_SCN_MEM_FARDATA = 0x00008000
|
|
IMAGE_SCN_MEM_PURGEABLE = 0x00020000
|
|
IMAGE_SCN_MEM_16BIT = 0x00020000
|
|
IMAGE_SCN_MEM_LOCKED = 0x00040000
|
|
IMAGE_SCN_MEM_PRELOAD = 0x00080000
|
|
IMAGE_SCN_ALIGN_1BYTES = 0x00100000
|
|
IMAGE_SCN_ALIGN_2BYTES = 0x00200000
|
|
IMAGE_SCN_ALIGN_4BYTES = 0x00300000
|
|
IMAGE_SCN_ALIGN_8BYTES = 0x00400000
|
|
IMAGE_SCN_ALIGN_16BYTES = 0x00500000
|
|
IMAGE_SCN_ALIGN_32BYTES = 0x00600000
|
|
IMAGE_SCN_ALIGN_64BYTES = 0x00700000
|
|
IMAGE_SCN_ALIGN_128BYTES = 0x00800000
|
|
IMAGE_SCN_ALIGN_256BYTES = 0x00900000
|
|
IMAGE_SCN_ALIGN_512BYTES = 0x00A00000
|
|
IMAGE_SCN_ALIGN_1024BYTES = 0x00B00000
|
|
IMAGE_SCN_ALIGN_2048BYTES = 0x00C00000
|
|
IMAGE_SCN_ALIGN_4096BYTES = 0x00D00000
|
|
IMAGE_SCN_ALIGN_8192BYTES = 0x00E00000
|
|
IMAGE_SCN_ALIGN_MASK = 0x00F00000
|
|
IMAGE_SCN_LNK_NRELOC_OVFL = 0x01000000
|
|
IMAGE_SCN_MEM_DISCARDABLE = 0x02000000
|
|
IMAGE_SCN_MEM_NOT_CACHED = 0x04000000
|
|
IMAGE_SCN_MEM_NOT_PAGED = 0x08000000
|
|
IMAGE_SCN_MEM_SHARED = 0x10000000
|
|
IMAGE_SCN_MEM_EXECUTE = 0x20000000
|
|
IMAGE_SCN_MEM_READ = 0x40000000
|
|
IMAGE_SCN_MEM_WRITE = 0x80000000
|
|
|
|
# Flags for the UNWIND_INFO flags field from
|
|
# RUNTIME_FUNCTION defs
|
|
UNW_FLAG_NHANDLER = 0x0
|
|
UNW_FLAG_EHANDLER = 0x1
|
|
UNW_FLAG_UHANDLER = 0x2
|
|
UNW_FLAG_CHAININFO = 0x4
|
|
|
|
# Resource Types (https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types)
|
|
RT_CURSOR = 1
|
|
RT_BITMAP = 2
|
|
RT_ICON = 3
|
|
RT_MENU = 4
|
|
RT_DIALOG = 5
|
|
RT_STRING = 6
|
|
RT_FONTDIR = 7
|
|
RT_FONT = 8
|
|
RT_ACCELERATOR = 9
|
|
RT_RCDATA = 10
|
|
RT_MESSAGETABLE = 11
|
|
RT_GROUP_CURSOR = 12
|
|
RT_GROUP_ICON = 14
|
|
RT_VERSION = 16
|
|
RT_DLGINCLUDE = 17
|
|
RT_PLUGPLAY = 19
|
|
RT_VXD = 20
|
|
RT_ANICURSOR = 21
|
|
RT_ANIICON = 22
|
|
RT_HTML = 23
|
|
RT_MANIFEST = 24
|
|
|
|
|
|
def fourPad(off):
|
|
return (4 - (off % 4)) % 4
|
|
|
|
|
|
def uncompLen(bytez):
|
|
valu = bytez[0]
|
|
if valu <= 0x7F:
|
|
return 1, valu
|
|
elif valu & 0xC0 == 0x80:
|
|
return 2, struct.unpack('>H', bytes([valu & 0x3F, bytez[1]]))[0]
|
|
else:
|
|
return 4, struct.unpack('>I', bytes([valu & 0x3F] + bytez[1:4]))[0]
|
|
|
|
|
|
RT_DESC = {
|
|
RT_CURSOR: 'Hardware-dependent cursor resource',
|
|
RT_BITMAP: 'Bitmap resource',
|
|
RT_ICON: 'Hardware-dependent icon resource',
|
|
RT_MENU: 'Menu resource',
|
|
RT_DIALOG: 'Dialog box',
|
|
RT_STRING: 'String-table entry',
|
|
RT_FONTDIR: 'Font directory resource',
|
|
RT_FONT: 'Font resource',
|
|
RT_ACCELERATOR: 'Accelerator table',
|
|
RT_RCDATA: 'Application-defined resource (raw data)',
|
|
RT_MESSAGETABLE: 'Message-table entry',
|
|
RT_GROUP_CURSOR: 'Hardware-independent cursor resource',
|
|
RT_GROUP_ICON: 'Hardware-independent icon resource',
|
|
RT_VERSION: 'Version resource',
|
|
RT_DLGINCLUDE: 'Associate string with .rc file',
|
|
RT_PLUGPLAY: 'Plug and Play resource',
|
|
RT_VXD: 'VXD',
|
|
RT_ANICURSOR: 'Animated cursor',
|
|
RT_ANIICON: 'Animated icon',
|
|
RT_HTML: 'HTML resource',
|
|
RT_MANIFEST: 'Side-by-Side Assembly Manifest',
|
|
}
|
|
|
|
logger = logging.getLogger('vivisect')
|
|
|
|
|
|
class VS_VERSIONINFO:
|
|
'''
|
|
A simple (read-only) VS_VERSIONINFO parser
|
|
'''
|
|
def __init__(self, bytes):
|
|
self._version_info = {}
|
|
self._fixed_file_info = None
|
|
self._parseBytes(bytes)
|
|
|
|
def getVersionValue(self, key, default=None):
|
|
'''
|
|
Retrieve a key from the VS_VERSIONINFO data.
|
|
|
|
Example: vs.getVersionValue('FileVersion')
|
|
'''
|
|
return self._version_info.get(key, default)
|
|
|
|
def getVersionKeys(self):
|
|
'''
|
|
Return a list of the keys in this VS_VERSIONINFO struct.
|
|
|
|
Example: for keyname in vs.getVersionKeys(): print(keyname)
|
|
'''
|
|
return self._version_info.keys()
|
|
|
|
def getVersionItems(self):
|
|
'''
|
|
Return dictionary style key,val tuples for the version keys
|
|
in this VS_VERSIONINFO structure.
|
|
|
|
Example: for vskey,vsdata in vs.getVersionItems(): print(vskey,vsdata)
|
|
'''
|
|
return self._version_info.items()
|
|
|
|
def _parseBytes(self, bytes):
|
|
offset = 0
|
|
mysize, valsize, vstype = struct.unpack('<HHH', bytes[:6])
|
|
offset += 6
|
|
offset, vinfosig = self._eatStringAndAlign(bytes, offset)
|
|
if vinfosig != 'VS_VERSION_INFO':
|
|
Exception('Invalid VS_VERSION_INFO signature!: %s' % repr(vinfosig))
|
|
|
|
if valsize and valsize >= len(vs_pe.VS_FIXEDFILEINFO()):
|
|
ffinfo = vs_pe.VS_FIXEDFILEINFO()
|
|
ffinfo.vsParse(bytes[offset:offset+valsize])
|
|
self._fixed_file_info = ffinfo
|
|
|
|
offset += valsize
|
|
offmod = offset % 4
|
|
if offmod:
|
|
offset += (4 - offmod)
|
|
|
|
xmax = min(mysize, len(bytes))
|
|
i = 0
|
|
while offset < xmax and i < 2:
|
|
offset = self._stringFileInfo(bytes, offset)
|
|
i += 1
|
|
|
|
def _eatStringAndAlign(self, bytes, offset):
|
|
ret = b''
|
|
blen = len(bytes)
|
|
while bytes[offset:offset+2] != b'\x00\x00':
|
|
ret += bytes[offset:offset+2]
|
|
offset += 2
|
|
if offset >= blen:
|
|
break
|
|
# Add 2 for the null terminator
|
|
offset += 2
|
|
offmod = offset % 4
|
|
if offmod:
|
|
offset += (4 - offmod)
|
|
return offset, ret.decode('utf-16le')
|
|
|
|
def _stringFileInfo(self, bytes, offset):
|
|
xoffset = offset
|
|
mysize, valsize, valtype = struct.unpack('<HHH', bytes[xoffset:xoffset+6])
|
|
xoffset += 6
|
|
xoffset, sigstr = self._eatStringAndAlign(bytes, xoffset)
|
|
#if sigstr not in ('VarFileInfo','StringFileInfo'):
|
|
#raise Exception('Invalid StringFileInfo Key!: %s' % repr(sigstr))
|
|
|
|
xmax = offset + mysize
|
|
|
|
if sigstr == 'StringFileInfo':
|
|
while xoffset < xmax:
|
|
xoffset = self._stringTable(bytes, xoffset, mysize - (xoffset-offset))
|
|
|
|
elif sigstr == 'VarFileInfo':
|
|
while xoffset < xmax:
|
|
xoffset = self._varTable(bytes, xoffset, mysize - (xoffset-offset))
|
|
|
|
xmod = xoffset % 4
|
|
if xmod:
|
|
xoffset += (4 - xmod)
|
|
|
|
return xoffset
|
|
|
|
def _varTable(self, bytes, offset, size):
|
|
xmax = offset + size
|
|
xoffset = offset
|
|
mysize, valsize, valtype = struct.unpack('<HHH', bytes[xoffset:xoffset+6])
|
|
xoffset += 6
|
|
xoffset, varname = self._eatStringAndAlign(bytes, xoffset)
|
|
if xoffset + 4 > len(bytes):
|
|
return offset+size
|
|
varval = struct.unpack('<I', bytes[xoffset:xoffset+4])[0]
|
|
xoffset += 4
|
|
self._version_info[varname] = varval
|
|
return offset + size
|
|
|
|
def _stringTable(self, bytes, offset, size):
|
|
xmax = offset + size
|
|
xoffset = offset
|
|
mysize, valsize, valtype = struct.unpack('<HHH', bytes[offset:offset+6])
|
|
xoffset += 6
|
|
xoffset, hexcpage = self._eatStringAndAlign(bytes, xoffset)
|
|
while xoffset < xmax:
|
|
xoffset = self._stringData(bytes, xoffset)
|
|
if xoffset == -1:
|
|
break
|
|
|
|
xmod = xoffset % 4
|
|
if xmod:
|
|
xoffset += (4 - xmod)
|
|
return offset + size
|
|
|
|
def _stringData(self, bytes, offset):
|
|
'''
|
|
Parse out a "String" structure...
|
|
'''
|
|
xoffset = offset
|
|
mysize, valsize, stype = struct.unpack('<HHH', bytes[offset:offset+6])
|
|
|
|
if mysize == 0:
|
|
return -1
|
|
|
|
xoffset += 6
|
|
xoffset, strkey = self._eatStringAndAlign(bytes, xoffset)
|
|
|
|
# valsize is in words...
|
|
valsize *= 2
|
|
value = bytes[xoffset : xoffset + valsize ]
|
|
|
|
# Do utf16le decode if we're "textual data"
|
|
if stype == 1:
|
|
value = value.decode('utf-16le','ignore')
|
|
value = value.split('\x00')[0]
|
|
|
|
self._version_info[strkey] = value
|
|
|
|
# No matter what we parse, believe the headers...
|
|
return offset + mysize
|
|
|
|
class ResourceDirectory:
|
|
'''
|
|
Resources are sorted into a hierarchy which begins with
|
|
"type" and then "name/id" which still points to another
|
|
directory entry which has 1 child (id 1033) with data.
|
|
'''
|
|
def __init__(self, nameid=None):
|
|
self._rsrc_data = []
|
|
self._rsrc_nameid = nameid
|
|
self._rsrc_subdirs = {}
|
|
|
|
def addRsrcDirectory(self, nameid):
|
|
r = ResourceDirectory(nameid=nameid)
|
|
self._rsrc_subdirs[nameid] = r
|
|
return r
|
|
|
|
def addRsrcData(self, rva, size, langinfo):
|
|
self._rsrc_data.append( (rva, size, langinfo) )
|
|
|
|
def getDirById(self, name_id):
|
|
return self._rsrc_subdirs.get(name_id)
|
|
|
|
def getResourceDef(self, restype, name_id):
|
|
'''
|
|
This should *only* be called on the root node!
|
|
'''
|
|
typedir = self._rsrc_subdirs.get(restype)
|
|
if typedir is None:
|
|
return None
|
|
|
|
datadir = typedir._rsrc_subdirs.get(name_id)
|
|
if datadir is None:
|
|
return None
|
|
|
|
if len(datadir._rsrc_data) == 0:
|
|
return None
|
|
|
|
# The first entry in the datadir's data is the one
|
|
return datadir._rsrc_data[0]
|
|
|
|
def getDataEntries(self):
|
|
return self._rsrc_data
|
|
|
|
class PE(object):
|
|
|
|
def __init__(self, fd, inmem=False):
|
|
"""
|
|
Construct a PE object. use inmem=True if you are
|
|
using a MemObjFile or other "memory like" image.
|
|
"""
|
|
object.__init__(self)
|
|
self.inmem = inmem
|
|
self.filesize = None
|
|
self.min_rva = None
|
|
self.max_rva = None
|
|
|
|
if not inmem:
|
|
fd.seek(0, os.SEEK_END)
|
|
self.filesize = fd.tell()
|
|
fd.seek(0)
|
|
|
|
self.fd = fd
|
|
|
|
self.pe32p = False
|
|
self.psize = 4
|
|
self.high_bit_mask = 0x80000000
|
|
self.IMAGE_DOS_HEADER = vstruct.getStructure("pe.IMAGE_DOS_HEADER")
|
|
dosbytes = self.readAtOffset(0, len(self.IMAGE_DOS_HEADER))
|
|
self.IMAGE_DOS_HEADER.vsParse(dosbytes)
|
|
|
|
# Parse in a default 32 bit, and then check for 64...
|
|
nt = self.readStructAtOffset(self.IMAGE_DOS_HEADER.e_lfanew, "pe.IMAGE_NT_HEADERS")
|
|
magic = struct.unpack("<H", nt.OptionalHeader.Magic)[0]
|
|
if magic == PE32PLUS_MAGIC:
|
|
nt = self.readStructAtOffset(self.IMAGE_DOS_HEADER.e_lfanew, "pe.IMAGE_NT_HEADERS64")
|
|
self.pe32p = True
|
|
self.psize = 8
|
|
self.high_bit_mask = 0x8000000000000000
|
|
elif magic != PE32_MAGIC:
|
|
logger.warning('nt.OptionalHeader magic got invalid value of %x', magic)
|
|
|
|
self.IMAGE_NT_HEADERS = nt
|
|
|
|
def __del__(self):
|
|
try:
|
|
self.fd.close()
|
|
except:
|
|
pass # whatever. we're tearing down anyway
|
|
|
|
def getFileBytes(self):
|
|
'''
|
|
Return the bytes of the file as they currently exist from the view of the file descriptor-like object
|
|
|
|
But keeping in mind not to smash over the old location of the fd
|
|
'''
|
|
self.fd.flush()
|
|
old = self.fd.tell()
|
|
self.fd.seek(0)
|
|
byts = self.fd.read()
|
|
self.fd.seek(old)
|
|
return byts
|
|
|
|
def getPdataEntries(self):
|
|
sec = self.getSectionByName('.pdata')
|
|
if sec is None:
|
|
return ()
|
|
ret = []
|
|
rbytes = self.readAtRva(sec.VirtualAddress, sec.VirtualSize)
|
|
while len(rbytes):
|
|
f = vs_pe.IMAGE_RUNTIME_FUNCTION_ENTRY()
|
|
f.vsParse(rbytes)
|
|
rbytes = rbytes[len(f):]
|
|
ret.append(f)
|
|
return ret
|
|
|
|
def getDllName(self):
|
|
'''
|
|
Return the "dll name" from the Name field of the IMAGE_EXPORT_DIRECTORY
|
|
if one is present. If not, return None.
|
|
'''
|
|
if self.IMAGE_EXPORT_DIRECTORY is not None:
|
|
rawname = self.readAtRva(self.IMAGE_EXPORT_DIRECTORY.Name, 32)
|
|
if not rawname:
|
|
return None
|
|
|
|
try:
|
|
return rawname.partition(b'\x00')[0].decode('ascii')
|
|
except UnicodeDecodeError:
|
|
return None
|
|
return None
|
|
|
|
def getImports(self):
|
|
"""
|
|
Return the list of import tuples for this PE. The tuples
|
|
are in the format (rva, libname, funcname).
|
|
"""
|
|
return self.imports
|
|
|
|
def getDelayImports(self):
|
|
"""
|
|
Return the list of delay import tuples for this PE. The tuples
|
|
are in the format (rva, libname, funcname).
|
|
"""
|
|
return self.delayImports
|
|
|
|
def getExports(self):
|
|
|
|
"""
|
|
Return the list of exports in this PE. The list contains
|
|
tuples in the format; (rva, ord, name).
|
|
"""
|
|
return self.exports
|
|
|
|
def getForwarders(self):
|
|
"""
|
|
[ (rva, name, forwardname), ... ]
|
|
"""
|
|
return self.forwarders
|
|
|
|
def getSections(self):
|
|
return self.sections
|
|
|
|
def vaToRva(self, va):
|
|
rva = va - self.IMAGE_NT_HEADERS.OptionalHeader.ImageBase
|
|
assert rva >= 0
|
|
return rva
|
|
|
|
def vaToOffset(self, va):
|
|
if self.inmem:
|
|
return va
|
|
|
|
return self.rvaToOffset(self.vaToRva(va))
|
|
|
|
def rvaToOffset(self, rva):
|
|
if self.inmem:
|
|
return rva
|
|
if rva >= 0 and rva < self.IMAGE_NT_HEADERS.OptionalHeader.SizeOfHeaders:
|
|
return rva
|
|
for s in self.sections:
|
|
sbase = s.VirtualAddress
|
|
if s.SizeOfRawData + s.PointerToRawData > self.getMaxRva():
|
|
# SizeOfRawData can be misleading.
|
|
ssize = s.VirtualSize
|
|
else:
|
|
ssize = max(s.SizeOfRawData, s.VirtualSize)
|
|
if rva >= sbase and rva < sbase + ssize:
|
|
return s.PointerToRawData + (rva - sbase)
|
|
return 0
|
|
|
|
def offsetToRva(self, offset):
|
|
if self.inmem:
|
|
return offset
|
|
|
|
for s in self.sections:
|
|
sbase = s.PointerToRawData
|
|
if s.SizeOfRawData + s.PointerToRawData > self.getMaxRva():
|
|
# SizeOfRawData can be misleading.
|
|
ssize = s.VirtualSize
|
|
else:
|
|
ssize = max(s.SizeOfRawData, s.VirtualSize)
|
|
if sbase <= offset and offset < sbase + ssize:
|
|
return offset - s.PointerToRawData + s.VirtualAddress
|
|
return 0
|
|
|
|
|
|
|
|
def getSectionByName(self, name):
|
|
for s in self.getSections():
|
|
if s.Name.split("\x00", 1)[0] == name:
|
|
return s
|
|
return None
|
|
|
|
def readStructAtRva(self, rva, structname, check=False):
|
|
s = vstruct.getStructure(structname)
|
|
slen = len(s)
|
|
if check and not self.checkRva(rva, size=slen):
|
|
return None
|
|
bytes = self.readAtRva(rva, len(s))
|
|
if not bytes:
|
|
return None
|
|
|
|
s.vsParse(bytes)
|
|
return s
|
|
|
|
def readStructAtOffset(self, offset, structname):
|
|
s = vstruct.getStructure(structname)
|
|
sbytes = self.readAtOffset(offset, len(s))
|
|
if not sbytes:
|
|
return None
|
|
|
|
s.vsParse(sbytes)
|
|
return s
|
|
|
|
def getDataDirectory(self, idx):
|
|
return self.IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[idx]
|
|
|
|
def getResourceDef(self, rtype, name_id):
|
|
'''
|
|
Get the (rva, size, (codepage,langid,sublangid)) tuple for the specified
|
|
resource type/id combination. Returns None if not found.
|
|
'''
|
|
return self.ResourceRoot.getResourceDef(rtype, name_id)
|
|
|
|
def getResources(self):
|
|
'''
|
|
Get the (rtype, nameid, (rva, size, (codepage,langid,sublangid))) tuples for each
|
|
resource in the PE.
|
|
'''
|
|
ret = []
|
|
for rtype, subdir in self.ResourceRoot._rsrc_subdirs.items():
|
|
for nameid, subsubdir in subdir._rsrc_subdirs.items():
|
|
ret.append((rtype, nameid, subsubdir._rsrc_data[0]))
|
|
return ret
|
|
|
|
def readResource(self, rtype, name_id):
|
|
'''
|
|
Return the bytes which define the specified resource. Returns
|
|
None if not found.
|
|
'''
|
|
rsdef = self.getResourceDef(rtype, name_id)
|
|
if rsdef is None:
|
|
return None
|
|
rsrva, rssize, rscpage = rsdef
|
|
return self.readAtRva(rsrva, rssize)
|
|
|
|
def getPdbPath(self):
|
|
'''
|
|
Parse and return the Pdb path from the Code View 4.0 data
|
|
specified by the IMAGE_DEBUG_DIRECTORY strucutre, or None
|
|
if a pdb path is not present.
|
|
'''
|
|
ddir = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_DEBUG)
|
|
drva = ddir.VirtualAddress
|
|
dsize = ddir.Size
|
|
d = self.readStructAtRva(drva, 'pe.IMAGE_DEBUG_DIRECTORY', check=True)
|
|
if d is None:
|
|
return None
|
|
|
|
if d.Type != IMAGE_DEBUG_TYPE_CODEVIEW:
|
|
return None
|
|
|
|
if not self.checkRva(d.AddressOfRawData, size=d.SizeOfData):
|
|
return None
|
|
|
|
cv = vs_pe.CV_INFO_PDB70()
|
|
cv.vsParse( self.readAtRva(d.AddressOfRawData, d.SizeOfData))
|
|
if cv.CvSignature != 0x53445352:
|
|
return None
|
|
|
|
return cv.PdbFileName
|
|
|
|
def getVS_VERSIONINFO(self):
|
|
'''
|
|
Get a VS_VERSIONINFO object for this PE.
|
|
(returns None if version resource is not found)
|
|
'''
|
|
vbytes = self.readResource(RT_VERSION, 1)
|
|
if vbytes is None:
|
|
return None
|
|
return VS_VERSIONINFO(vbytes)
|
|
|
|
def parseResources(self):
|
|
|
|
self.ResourceRoot = ResourceDirectory()
|
|
|
|
# RP BUG FIX - Binaries can have a .rsrc section it doesn't mean that the .rsrc section contains the resource data we think it does
|
|
# validate .rsrc == RESOURCE Section by checking data directory entries...
|
|
dresc = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_RESOURCE)
|
|
if not dresc.VirtualAddress:
|
|
return
|
|
|
|
done = {}
|
|
rsrc_todo = [ (dresc.VirtualAddress, self.ResourceRoot), ]
|
|
|
|
while len(rsrc_todo):
|
|
rsrva, rsdirobj = rsrc_todo.pop()
|
|
rsdir = self.readStructAtRva( rsrva, 'pe.IMAGE_RESOURCE_DIRECTORY', check=True )
|
|
if rsdir is None:
|
|
continue
|
|
|
|
totcount = rsdir.NumberOfIdEntries + rsdir.NumberOfNamedEntries
|
|
# check if our to do is too many, limit borrowed from pefile
|
|
if totcount > 4096:
|
|
continue
|
|
|
|
offset = len(rsdir)
|
|
for i in range(totcount):
|
|
dentrva = rsrva + offset
|
|
|
|
dirent = self.readStructAtRva( dentrva, 'pe.IMAGE_RESOURCE_DIRECTORY_ENTRY', check=True )
|
|
if dirent is None:
|
|
break
|
|
|
|
# We use name/id interchangably in the python dict...
|
|
name_id = None
|
|
if dirent.Name & 0x80000000: # If high bit is set, it's a string!
|
|
namerva = dresc.VirtualAddress + (dirent.Name & 0x7fffffff)
|
|
namelen_bytes = self.readAtRva(namerva, 2)
|
|
if not namelen_bytes:
|
|
continue
|
|
namelen = struct.unpack('<H', namelen_bytes)[0]
|
|
name_raw = self.readAtRva(namerva + 2, namelen * 2)
|
|
if not name_raw:
|
|
continue
|
|
name_id = name_raw.decode('utf-16le', 'ignore')
|
|
if not name_id:
|
|
name_id = dirent.Name
|
|
|
|
else:
|
|
name_id = dirent.Name
|
|
|
|
# if OffsetToData & IMAGE_RESOURCE_DATA_IS_DIRECTORY then we have another directory
|
|
if dirent.OffsetToData & 0x80000000:
|
|
# This points to a subdirectory
|
|
subdir = rsdirobj.addRsrcDirectory(name_id)
|
|
doffset = dirent.OffsetToData & 0x7fffffff
|
|
drva = dresc.VirtualAddress + doffset
|
|
# XXX - prevent infinite loop by making sure the RVA isnt in our list to visit
|
|
# and we aren't currently examining it.
|
|
if doffset and rsrva != drva and not done.get(drva):
|
|
rsrc_todo.append( (drva, subdir) )
|
|
done[drva] = 1
|
|
|
|
else:
|
|
subdata = self.readStructAtRva( dresc.VirtualAddress + dirent.OffsetToData, 'pe.IMAGE_RESOURCE_DATA_ENTRY')
|
|
# RP BUG FIX - sanity check the subdata
|
|
if subdata and self.checkRva(subdata.OffsetToData, size=subdata.Size):
|
|
# sometimes people are bad and they lie to us
|
|
try:
|
|
langid = name_id & 0x3ff
|
|
sublangid = name_id >> 10
|
|
except:
|
|
langid = None
|
|
sublangid = None
|
|
langinfo = (subdata.CodePage, langid, sublangid)
|
|
rsdirobj.addRsrcData(subdata.OffsetToData, subdata.Size, langinfo)
|
|
|
|
offset += len(dirent)
|
|
|
|
def parseSections(self):
|
|
|
|
self.sections = []
|
|
off = self.IMAGE_DOS_HEADER.e_lfanew + len(self.IMAGE_NT_HEADERS)
|
|
off -= len(self.IMAGE_NT_HEADERS.OptionalHeader.DataDirectory)
|
|
off += self.IMAGE_NT_HEADERS.OptionalHeader.NumberOfRvaAndSizes * len(vstruct.getStructure("pe.IMAGE_DATA_DIRECTORY"))
|
|
|
|
secsize = len(vstruct.getStructure("pe.IMAGE_SECTION_HEADER"))
|
|
hdrsize = secsize * self.IMAGE_NT_HEADERS.FileHeader.NumberOfSections
|
|
sbytes = self.readAtOffset(off, hdrsize)
|
|
|
|
if len(sbytes) != hdrsize:
|
|
raise v_exc.CorruptPeFile("truncated section headers")
|
|
|
|
indx = off
|
|
while sbytes:
|
|
s = vstruct.getStructure("pe.IMAGE_SECTION_HEADER")
|
|
s.vsParse(sbytes[:secsize])
|
|
s.vsSetMeta('Offset', indx)
|
|
indx += secsize
|
|
self.sections.append(s)
|
|
sbytes = sbytes[secsize:]
|
|
|
|
def readRvaFormat(self, fmt, rva):
|
|
size = struct.calcsize(fmt)
|
|
fbytes = self.readAtRva(rva, size)
|
|
return struct.unpack(fmt, fbytes)
|
|
|
|
def readAtVa(self, va, size, shortok=False):
|
|
offset = self.vaToOffset(va)
|
|
return self.readAtOffset(offset, size, shortok)
|
|
|
|
def readAtRva(self, rva, size, shortok=False):
|
|
offset = self.rvaToOffset(rva)
|
|
return self.readAtOffset(offset, size, shortok)
|
|
|
|
def readAtOffset(self, offset, size, shortok=False):
|
|
ret = b""
|
|
self.fd.seek(offset)
|
|
while len(ret) != size:
|
|
rlen = size - len(ret)
|
|
x = self.fd.read(rlen)
|
|
if x == b"":
|
|
if not shortok:
|
|
return None
|
|
return ret
|
|
ret += x
|
|
return ret
|
|
|
|
def parseLoadConfig(self):
|
|
self.IMAGE_LOAD_CONFIG = None
|
|
cdir = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG)
|
|
rva = cdir.VirtualAddress
|
|
# RP BUG FIX - validate config directory
|
|
if self.checkRva(rva, size=cdir.Size):
|
|
self.IMAGE_LOAD_CONFIG = self.readStructAtRva(rva, "pe.IMAGE_LOAD_CONFIG_DIRECTORY")
|
|
|
|
def readPointerAtOffset(self, off):
|
|
fmt = "<L"
|
|
if self.psize == 8:
|
|
fmt = "<Q"
|
|
return struct.unpack(fmt, self.readAtOffset(off, self.psize))[0]
|
|
|
|
def readPointerAtVa(self, va):
|
|
off = self.vaToOffset(va)
|
|
return self.readPointerAtOffset(off)
|
|
|
|
def readPointerAtRva(self, rva):
|
|
off = self.rvaToOffset(rva)
|
|
return self.readPointerAtOffset(off)
|
|
|
|
def getMinRva(self):
|
|
'''
|
|
Minimum RVA is the smallest virtual address that might be observed.
|
|
'''
|
|
if not self.min_rva:
|
|
self.min_rva = min(map(lambda sec: sec.VirtualAddress, self.getSections()))
|
|
return self.min_rva
|
|
|
|
def getMaxRva(self):
|
|
'''
|
|
Maximum RVA is the largest virtual address that might be observed.
|
|
'''
|
|
if not self.max_rva:
|
|
max_sec = 0
|
|
for sec in self.getSections():
|
|
sec_end = sec.VirtualAddress + sec.VirtualSize
|
|
align = self.IMAGE_NT_HEADERS.OptionalHeader.SectionAlignment
|
|
if (align > 0):
|
|
sec_end = align * (int(sec_end / align) + 1)
|
|
max_sec = max(max_sec, sec_end)
|
|
self.max_rva = max_sec
|
|
return self.max_rva
|
|
|
|
def checkRva(self, rva, size=None):
|
|
'''
|
|
Make sure an RVA falls inside the valid mapped range
|
|
for the file. (also make sure it's not 0...)
|
|
'''
|
|
if rva == 0:
|
|
return False
|
|
|
|
isize = self.getMaxRva()
|
|
|
|
if rva > isize:
|
|
#raise Exception('too high! %d > %d' % (rva, isize))
|
|
return False
|
|
|
|
if size is not None and (rva + size) > isize:
|
|
#raise Exception('too big! %d > %d' % (rva+size, isize))
|
|
return False
|
|
|
|
return True
|
|
|
|
def readStringAtRva(self, rva, maxsize=None):
|
|
ret = b''
|
|
while True:
|
|
if maxsize and maxsize <= len(ret):
|
|
break
|
|
x = self.readAtRva(rva, 1)
|
|
if x == b'\x00' or x is None:
|
|
break
|
|
ret += x
|
|
rva += 1
|
|
return ret
|
|
|
|
def readStringAtVa(self, va, maxsize=None):
|
|
return self.readStringAtRva(self.vaToRva(va), maxsize=maxsize)
|
|
|
|
def parseImports(self):
|
|
idir = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_IMPORT)
|
|
|
|
# RP BUG FIX - invalid IAT entry will point of range of file
|
|
irva = idir.VirtualAddress
|
|
x = self.readStructAtRva(irva, 'pe.IMAGE_IMPORT_DIRECTORY', check=True)
|
|
if x is None:
|
|
self.imports = []
|
|
return
|
|
|
|
self.imports = self.parseImportTable(x, irva, flavor="import table", uses_rva=True)
|
|
|
|
def parseDelayImports(self):
|
|
didir = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT)
|
|
|
|
# RP BUG FIX - invalid IAT entry will point of range of file
|
|
irva = didir.VirtualAddress
|
|
x = self.readStructAtRva(irva, 'pe.IMAGE_DELAY_IMPORT_DIRECTORY', check=True)
|
|
if x is None:
|
|
self.delayImports = []
|
|
return
|
|
|
|
# `doesDelayImportTableUseRVAs` modifies the given vstruct,
|
|
# so we parse and provide another copy.
|
|
y = self.readStructAtRva(irva, 'pe.IMAGE_DELAY_IMPORT_DIRECTORY', check=True)
|
|
uses_rva = self.doesDelayImportTableUseRVAs(y, irva)
|
|
if not uses_rva:
|
|
logger.debug("delayed imports: delay import table uses VAs (unusual, might be VS6)")
|
|
|
|
self.delayImports = self.parseImportTable(x, irva, flavor="delay import table", uses_rva=uses_rva)
|
|
|
|
def doesDelayImportTableUseRVAs(self, x, irva):
|
|
"""
|
|
return True if the delay import table at the given irva appears to use RVAs.
|
|
this is the common case. but, VS6 had a bug in which the delay import table used VAs, instead.
|
|
there's no standard, because its up to the compiler to insert the delayed import handling code.
|
|
ref: https://stackoverflow.com/questions/40570909/difference-between-bound-imports-and-delayed-imports-in-pe-header
|
|
"""
|
|
isize = len(x)
|
|
|
|
# we'll loop through all the DLL entries in the delay import table
|
|
# casting a vote if the name pointer appears to be an RVA or not.
|
|
# then the case with the most votes at the end wins.
|
|
# this handles the case when a small number of the pointers may be hard to interpret.
|
|
votes = []
|
|
while True:
|
|
if x.rvaDLLName == 0:
|
|
break
|
|
|
|
ptr = x.rvaINT
|
|
if ptr == 0:
|
|
ptr = x.rvaIAT
|
|
|
|
if ptr == 0:
|
|
break
|
|
|
|
# cast a vote:
|
|
# does ptr appear to be an RVA or VA?
|
|
if ptr < self.getMinRva():
|
|
# not a valid VA (too small), so must be a RVA
|
|
votes.append(True)
|
|
|
|
elif ptr >= self.getMaxRva():
|
|
# we have no idea: address is neither RVA or VA
|
|
# this is probably corrupt.
|
|
pass
|
|
|
|
elif self.getMinRva() <= ptr < self.getMaxRva():
|
|
# probably a VA,
|
|
# though if we're dealing with a large program,
|
|
# the range of VA and RVA values may overlap.
|
|
|
|
# when the file is small, RVA and VA ranges don't overlap:
|
|
#
|
|
# base addr
|
|
# 0 min VA max VA
|
|
# | |------------|
|
|
# |------------|
|
|
# min RVA max RVA
|
|
#
|
|
#
|
|
# but when the file is large relative to the base address:
|
|
#
|
|
# base addr
|
|
# 0 min VA max VA
|
|
# | |----------------------|
|
|
# |----------------------|
|
|
# min RVA max RVA
|
|
#
|
|
# ^^^^^^^^^^^^^ we can't tell if these addresses are RVA or VA
|
|
|
|
if (self.getMaxRva() - self.getMinRva()) > self.getMinRva():
|
|
# the VA and RVA range overlap, and
|
|
# we can't directly tell if this is an RVA or VA.
|
|
#
|
|
# so, we use a couple heuristics to make our best guess.
|
|
try:
|
|
self.readPointerAtRva(ptr)
|
|
except:
|
|
can_deref_as_rva = False
|
|
else:
|
|
can_deref_as_rva = True
|
|
|
|
try:
|
|
self.readPointerAtVa(ptr)
|
|
except:
|
|
can_deref_as_va = False
|
|
else:
|
|
can_deref_as_va = True
|
|
|
|
if (can_deref_as_rva, can_deref_as_va) == (True, False):
|
|
# can only be interpreted as an RVA
|
|
votes.append(True)
|
|
|
|
elif (can_deref_as_rva, can_deref_as_va) == (False, True):
|
|
# can only be interpreted as an VA
|
|
votes.append(False)
|
|
|
|
elif (can_deref_as_rva, can_deref_as_va) == (False, False):
|
|
# cannot be interpreted as either VA nor RVA
|
|
pass
|
|
|
|
elif (can_deref_as_rva, can_deref_as_va) == (True, True):
|
|
# both VA and RVA interpretation could work.
|
|
# so, we assume the difference between
|
|
# the location of the pointer and the pointed-to location are fairly similar.
|
|
# this is because the import table probably points to a nearby structure,
|
|
# e.g., in the same section.
|
|
#
|
|
# so, is `abs(*ptr - ptr) < min rva`?
|
|
# this should be the case for everything except:
|
|
# - REALLY large import tables, and
|
|
# - import tables split across more than one large section
|
|
#
|
|
# dereference as an RVA, and if the heuristic works, vote yes.
|
|
# otherwise, its probably a VA.
|
|
deref = self.readPointerAtRva(ptr)
|
|
if abs(deref - ptr) < self.getMinRva():
|
|
# the data is nearby, so the delta is small, so its probably an RVA.
|
|
votes.append(True)
|
|
else:
|
|
# the treating the data as an RVA resulting in a large delta,
|
|
# so its probably a VA.
|
|
votes.append(False)
|
|
else:
|
|
# for clarity: all the cases are explicit above.
|
|
raise Exception("impossible")
|
|
|
|
else:
|
|
# its not an RVA,
|
|
# and there' no overlap between valid VA and RVA ranges,
|
|
# so it must be a VA.
|
|
votes.append(False)
|
|
|
|
else:
|
|
# for clarity: all the cases are explicit above.
|
|
raise Exception("impossible")
|
|
|
|
irva += isize
|
|
|
|
if not self.checkRva(irva, size=isize):
|
|
# if import table is at the end of the file
|
|
# we may run to the end.
|
|
break
|
|
|
|
x.vsParse(self.readAtRva(irva, isize))
|
|
|
|
logger.debug("delayed imports: votes for RVAs: %d", sum([1 for vote in votes if vote]))
|
|
logger.debug("delayed imports: votes for VAs: %d", sum([1 for vote in votes if not vote]))
|
|
|
|
# more votes for RVA than for VA
|
|
return sum([1 for vote in votes if vote]) > sum([1 for vote in votes if not vote])
|
|
|
|
def parseImportTable(self, x, irva, flavor="import table", uses_rva=True):
|
|
'''
|
|
Parse a standard or delayed import table, adding to imports_list.
|
|
Start with x and irva set to the first entry in the table.
|
|
'''
|
|
if flavor not in ("import table", "delay import table"):
|
|
raise ValueError("unexpected flavor: " + flavor)
|
|
|
|
imports_list = []
|
|
isize = len(x)
|
|
|
|
while True:
|
|
if flavor == "import table":
|
|
entry_name = x.Name
|
|
elif flavor == "delay import table":
|
|
entry_name = x.rvaDLLName
|
|
else:
|
|
raise ValueError("unexpected flavor: " + flavor)
|
|
|
|
if not self.checkRva(entry_name):
|
|
break
|
|
|
|
if uses_rva:
|
|
entry_name_rva = entry_name
|
|
else:
|
|
entry_name_rva = self.vaToRva(entry_name)
|
|
|
|
# RP BUG FIX - we can't assume that we have 256 bytes to read
|
|
libname = self.readStringAtRva(entry_name_rva, maxsize=256).decode('utf-8')
|
|
idx = 0
|
|
|
|
if flavor == "import table":
|
|
imp_by_name = x.OriginalFirstThunk
|
|
if imp_by_name == 0:
|
|
imp_by_name = x.FirstThunk
|
|
save_name = x.FirstThunk
|
|
elif flavor == "delay import table":
|
|
imp_by_name = x.rvaINT
|
|
if imp_by_name == 0:
|
|
imp_by_name = x.rvaIAT
|
|
save_name = x.rvaIAT
|
|
else:
|
|
raise ValueError("unexpected flavor: " + flavor)
|
|
|
|
if uses_rva and not self.checkRva(imp_by_name):
|
|
break
|
|
|
|
if uses_rva:
|
|
imp_by_name_rva = imp_by_name
|
|
else:
|
|
imp_by_name_rva = self.vaToRva(imp_by_name)
|
|
|
|
while True:
|
|
arrayoff = self.psize * idx
|
|
if self.filesize is not None and arrayoff > self.filesize:
|
|
# we've read more pointers than could possibly be in this file
|
|
# so we probably already put garbage in the list.
|
|
# therefore, bail with empty results.
|
|
return []
|
|
|
|
ibn_addr = self.readPointerAtRva(imp_by_name_rva+arrayoff)
|
|
|
|
if ibn_addr == 0:
|
|
break
|
|
|
|
if ibn_addr & self.high_bit_mask:
|
|
funcname = ordlookup.ordLookup(libname, ibn_addr & 0x7fffffff)
|
|
|
|
elif uses_rva and not self.checkRva(ibn_addr):
|
|
break
|
|
|
|
else:
|
|
if uses_rva:
|
|
ibn_rva = ibn_addr
|
|
else:
|
|
ibn_rva = self.vaToRva(ibn_addr)
|
|
|
|
# RP BUG FIX - we can't use this API on this call because we can have binaries that put their import table
|
|
# right at the end of the file, statically saying the imported function name is 128 will cause use to potentially
|
|
# over run our read and traceback...
|
|
diff = self.getMaxRva() - ibn_addr - 2
|
|
ibn = vstruct.getStructure("pe.IMAGE_IMPORT_BY_NAME")
|
|
ibn.vsGetField('Name').vsSetLength(min(diff, 128))
|
|
|
|
|
|
bytes = self.readAtRva(ibn_rva, len(ibn), shortok=True)
|
|
|
|
if not bytes:
|
|
break
|
|
try:
|
|
ibn.vsParse(bytes)
|
|
except:
|
|
idx+=1
|
|
continue
|
|
|
|
try:
|
|
funcname = ibn.Name
|
|
except UnicodeDecodeError:
|
|
funcname = None
|
|
logger.warning("pe: failed to read import name at RVA 0x%x", ibn_rva)
|
|
|
|
if uses_rva:
|
|
import_rva = save_name + arrayoff
|
|
else:
|
|
import_rva = self.vaToRva(save_name + arrayoff)
|
|
|
|
if funcname is not None:
|
|
imports_list.append((import_rva, libname, funcname))
|
|
|
|
idx += 1
|
|
|
|
irva += isize
|
|
|
|
# RP BUG FIX - if the import table is at the end of the file we can't count on the ending to be null
|
|
if not self.checkRva(irva, size=isize):
|
|
break
|
|
|
|
x.vsParse(self.readAtRva(irva, isize))
|
|
|
|
return imports_list
|
|
|
|
def getRelocations(self):
|
|
"""
|
|
Return the list of RVA base-relocations in this PE.
|
|
"""
|
|
return self.relocations
|
|
|
|
def parseRelocations(self):
|
|
self.relocations = []
|
|
edir = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_BASERELOC)
|
|
rva = edir.VirtualAddress
|
|
rsize = edir.Size
|
|
|
|
# RP BUG FIX - don't watn to read past the end of the file
|
|
if not self.checkRva(rva):
|
|
return
|
|
|
|
reloff = self.rvaToOffset(rva)
|
|
relbytes = self.readAtOffset(reloff, rsize)
|
|
|
|
while relbytes:
|
|
# bounce if we have less than 8 bytes to unpack
|
|
if len(relbytes) < 8:
|
|
return
|
|
|
|
pageva, chunksize = struct.unpack("<II", relbytes[:8])
|
|
relcnt = (chunksize - 8) // 2
|
|
|
|
# if chunksize == 0 bail
|
|
if not chunksize:
|
|
logger.warning("PE: corrupt relocation table: chunk size is 0")
|
|
return
|
|
|
|
# RP BUG FIX - sometimes the chunksize is invalid we do a quick check to make sure we dont overrun the buffer
|
|
if chunksize > len(relbytes):
|
|
logger.warning("PE: corrupt relocation table: chunk size > table size")
|
|
return
|
|
|
|
if relcnt < 0:
|
|
logger.warning("PE: corrupt relocation table: negative relocation count")
|
|
return
|
|
|
|
for roffset in range(8, min(chunksize, len(relbytes)), 2):
|
|
r = struct.unpack_from("<H", relbytes, roffset)[0]
|
|
rtype = r >> 12
|
|
roff = r & 0xfff
|
|
self.relocations.append((pageva+roff, rtype))
|
|
|
|
relbytes = relbytes[chunksize:]
|
|
|
|
def getExportName(self):
|
|
'''
|
|
Return the name of this file acording to it's export entry.
|
|
(if there are no exports, return None)
|
|
|
|
'''
|
|
e = self.IMAGE_EXPORT_DIRECTORY
|
|
if e is None:
|
|
return None
|
|
|
|
return self.readAtRva(e.Name, 128).split('\x00')[0]
|
|
|
|
def parseExports(self):
|
|
|
|
# Initialize our required locals.
|
|
self.exports = []
|
|
self.forwarders = []
|
|
self.IMAGE_EXPORT_DIRECTORY = None
|
|
|
|
edir = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_EXPORT)
|
|
poff = self.rvaToOffset(edir.VirtualAddress)
|
|
|
|
if poff == 0: # No exports...
|
|
return
|
|
|
|
self.IMAGE_EXPORT_DIRECTORY = self.readStructAtOffset(poff, "pe.IMAGE_EXPORT_DIRECTORY")
|
|
if not self.IMAGE_EXPORT_DIRECTORY:
|
|
return
|
|
|
|
funcoff = self.rvaToOffset(self.IMAGE_EXPORT_DIRECTORY.AddressOfFunctions)
|
|
funcsize = 4 * self.IMAGE_EXPORT_DIRECTORY.NumberOfFunctions
|
|
nameoff = self.rvaToOffset(self.IMAGE_EXPORT_DIRECTORY.AddressOfNames)
|
|
namesize = 4 * self.IMAGE_EXPORT_DIRECTORY.NumberOfNames
|
|
ordoff = self.rvaToOffset(self.IMAGE_EXPORT_DIRECTORY.AddressOfOrdinals)
|
|
ordsize = 2 * self.IMAGE_EXPORT_DIRECTORY.NumberOfNames
|
|
|
|
# RP BUG FIX - sanity check the exports before reading
|
|
# FH BUG FIX - ordoff and nameoff must both be set (named function exports)
|
|
# or both be null (unnamed function exports)
|
|
if not funcoff or funcsize > 0x7FFF or ((ordoff > 0) ^ (nameoff > 0)):
|
|
self.IMAGE_EXPORT_DIRECTORY = None
|
|
return
|
|
|
|
if funcsize == 0:
|
|
self.IMAGE_EXPORT_DIRECTORY = None
|
|
return
|
|
|
|
funcbytes = self.readAtOffset(funcoff, funcsize)
|
|
|
|
if not funcbytes:
|
|
self.IMAGE_EXPORT_DIRECTORY = None
|
|
return
|
|
|
|
funclist = struct.unpack("%dI" % (len(funcbytes) / 4), funcbytes)
|
|
|
|
# named function exports
|
|
if nameoff and ordoff:
|
|
namebytes = self.readAtOffset(nameoff, namesize)
|
|
ordbytes = self.readAtOffset(ordoff, ordsize)
|
|
|
|
namelist = struct.unpack("%dI" % (len(namebytes) / 4), namebytes)
|
|
ordlist = struct.unpack("%dH" % (len(ordbytes) / 2), ordbytes)
|
|
|
|
for i in range(len(namelist)):
|
|
|
|
ordl = ordlist[i]
|
|
nameoff = self.rvaToOffset(namelist[i])
|
|
if ordl > len(funclist):
|
|
self.IMAGE_EXPORT_DIRECTORY = None
|
|
return
|
|
|
|
funcoff = funclist[ordl]
|
|
ffoff = self.rvaToOffset(funcoff)
|
|
|
|
name = None
|
|
|
|
if nameoff != 0:
|
|
name = self.readAtOffset(nameoff, 256, shortok=True).split(b"\x00", 1)[0]
|
|
else:
|
|
name = b'ord_%.4x' % ordl
|
|
|
|
# RP BUG FIX - Export forwarding range check is done using RVA's
|
|
if funcoff >= edir.VirtualAddress and funcoff < edir.VirtualAddress + edir.Size:
|
|
fwdname = self.readAtRva(funcoff, 260, shortok=True).split(b'\x00', 1)[0]
|
|
self.forwarders.append((funclist[ordl], name.decode('utf-8'), fwdname))
|
|
else:
|
|
try:
|
|
self.exports.append((funclist[ordl], ordl, name.decode('utf-8')))
|
|
except UnicodeDecodeError:
|
|
logger.warning('Invalid name for export ordinal %i: %s', ordl, name[:16].hex())
|
|
|
|
# unnamed function exports
|
|
else:
|
|
# sanity check length of array containing export functions
|
|
if len(funclist) != self.IMAGE_EXPORT_DIRECTORY.NumberOfFunctions:
|
|
self.IMAGE_EXPORT_DIRECTORY = None
|
|
return
|
|
|
|
for i in range(len(funclist)):
|
|
funcoff = funclist[i]
|
|
|
|
# The function array will contain X elements, where X equals (highest
|
|
# ordinal number - lowest ordinal number). For example, a PE with ordinal
|
|
# exports of 0x10, 0x14, and 0x18 will contain 0x9 elements, with elements
|
|
# 0x0, 0x4, and 0x8 containing the relative offset of the corresponding
|
|
# exported function. An element with a value of 0 indicates the element in
|
|
# the array is a placeholder to preserve the length of the array.
|
|
if funcoff > 0:
|
|
ordl = self.IMAGE_EXPORT_DIRECTORY.Base + i
|
|
self.exports.append((funcoff, ordl, None))
|
|
|
|
def getSignature(self):
|
|
'''
|
|
Returns the SignatureEntry vstruct if the pe has an embedded
|
|
certificate, None if the magic bytes are NOT set in the security
|
|
directory entry AND the size of the signature entry is less than 0.
|
|
'''
|
|
ds = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_SECURITY)
|
|
|
|
va = ds.VirtualAddress
|
|
size = ds.Size
|
|
if size <= 0:
|
|
return None
|
|
|
|
bytez = self.readAtOffset(va, size)
|
|
if not bytez:
|
|
return None
|
|
|
|
se = vstruct.getStructure('pe.SignatureEntry')
|
|
se.vsParse(bytez)
|
|
|
|
if se.magic != "\x00\x02\x02\x00":
|
|
return None
|
|
|
|
return se
|
|
|
|
def getSignCertInfo(self):
|
|
|
|
sig = self.getSignature()
|
|
|
|
if sig is None:
|
|
return ()
|
|
|
|
# Runtime import these so they are optional dependancies
|
|
import pyasn1.type.univ
|
|
import pyasn1.type.namedtype
|
|
import pyasn1.codec.der.decoder
|
|
import pyasn1.codec.der.encoder
|
|
import pyasn1_modules.rfc2315
|
|
|
|
substrate = sig.pkcs7
|
|
contentInfo, rest = pyasn1.codec.der.decoder.decode(substrate, asn1Spec=pyasn1_modules.rfc2315.ContentInfo())
|
|
|
|
if rest: substrate = substrate[:-len(rest)]
|
|
|
|
contentType = contentInfo.getComponentByName('contentType')
|
|
|
|
contentInfoMap = {
|
|
(1, 2, 840, 113549, 1, 7, 1): pyasn1_modules.rfc2315.Data(),
|
|
(1, 2, 840, 113549, 1, 7, 2): pyasn1_modules.rfc2315.SignedData(),
|
|
(1, 2, 840, 113549, 1, 7, 3): pyasn1_modules.rfc2315.EnvelopedData(),
|
|
(1, 2, 840, 113549, 1, 7, 4): pyasn1_modules.rfc2315.SignedAndEnvelopedData(),
|
|
(1, 2, 840, 113549, 1, 7, 5): pyasn1_modules.rfc2315.DigestedData(),
|
|
(1, 2, 840, 113549, 1, 7, 6): pyasn1_modules.rfc2315.EncryptedData()
|
|
}
|
|
|
|
seqTypeMap = {
|
|
|
|
(2,5,4,3): 'CN',
|
|
(2,5,4,7): 'L',
|
|
(2,5,4,10): 'O',
|
|
(2,5,4,11): 'OU',
|
|
(1,2,840,113549,1,9,1): 'E',
|
|
(2,5,4,6): 'C',
|
|
(2,5,4,8): 'ST',
|
|
(2,5,4,9): 'STREET',
|
|
(2,5,4,12): 'TITLE',
|
|
(2,5,4,42): 'G',
|
|
(2,5,4,43): 'I',
|
|
(2,5,4,4): 'SN',
|
|
(0,9,2342,19200300,100,1,25): 'DC',
|
|
}
|
|
|
|
content, _ = pyasn1.codec.der.decoder.decode(
|
|
contentInfo.getComponentByName('content'),
|
|
asn1Spec=contentInfoMap[contentType]
|
|
)
|
|
|
|
a = content.getComponentByName('certificates')
|
|
|
|
certs = []
|
|
for i in a:
|
|
|
|
cbytes = pyasn1.codec.der.encoder.encode( i['certificate'] )
|
|
|
|
iparts = []
|
|
for _, rdnsequence in i["certificate"]["tbsCertificate"]["issuer"].items():
|
|
for rdn in rdnsequence:
|
|
rtype = rdn[0]["type"]
|
|
rvalue = rdn[0]["value"][2:]
|
|
iparts.append('%s=%s' % ( seqTypeMap.get( rtype, 'UNK'), rvalue))
|
|
|
|
issuer = ','.join( iparts )
|
|
|
|
sparts = []
|
|
for _, rdnsequence in i["certificate"]["tbsCertificate"]["subject"].items():
|
|
for rdn in rdnsequence:
|
|
rtype = rdn[0]["type"]
|
|
rvalue = rdn[0]["value"][2:]
|
|
sparts.append('%s=%s' % ( seqTypeMap.get( rtype, 'UNK'), rvalue))
|
|
|
|
subject = ','.join(sparts)
|
|
|
|
serial = int(i["certificate"]["tbsCertificate"]["serialNumber"])
|
|
|
|
cert = { 'subject':subject, 'issuer':issuer, 'serial':serial, 'bytes':cbytes }
|
|
certs.append( cert )
|
|
|
|
return certs
|
|
|
|
def __repr__(self, verbose=False):
|
|
# leave verbose for now but deprecate it
|
|
out = []
|
|
out.append("PE Binary:")
|
|
dllName = self.getDllName()
|
|
out.append("DllName: %r" % dllName)
|
|
|
|
out.append(self.IMAGE_DOS_HEADER.tree())
|
|
out.append(self.IMAGE_NT_HEADERS.tree())
|
|
out.append('\nSections')
|
|
for sec in self.getSections():
|
|
out.append(sec.tree())
|
|
|
|
rscs = self.getResources()
|
|
if rscs:
|
|
out.append('\nResources:')
|
|
for rtype, nameid, (rva, size, (codepage, langid, sublangid)) in rscs:
|
|
out.append('\n0x%.8x (Type: %s)' % (rva, RT_DESC.get(rtype, str(rtype))))
|
|
out.append(' Name ID: %d' % nameid)
|
|
out.append(' Size: %d' % size)
|
|
out.append(' CodePage: %d' % codepage)
|
|
out.append(' Lang ID: %d' % langid)
|
|
out.append(' Sublang ID: %d' % sublangid)
|
|
|
|
pdbpath = self.getPdbPath()
|
|
if pdbpath:
|
|
out.append("\nPDB Path: %r" % pdbpath)
|
|
|
|
imps = self.getImports()
|
|
if imps:
|
|
out.append('\nImports:')
|
|
for imp in imps:
|
|
out.append('0x%.8x %s\t%s' % imp)
|
|
|
|
imps = self.getDelayImports()
|
|
if imps:
|
|
out.append('\nDelayed Imports:')
|
|
for imp in imps:
|
|
out.append('0x%.8x %s\t%s' % imp)
|
|
|
|
exps = self.getExports()
|
|
if exps:
|
|
out.append('\nExports:')
|
|
for exp in self.getExports():
|
|
out.append('0x%.8x %s\t%s' % exp)
|
|
|
|
return '\n'.join(out)
|
|
|
|
def __getattr__(self, name):
|
|
"""
|
|
Use a getattr over-ride to allow "on demand" parsing of particular sections.
|
|
"""
|
|
if name == "exports":
|
|
self.parseExports()
|
|
return self.exports
|
|
|
|
elif name == "IMAGE_IMPORT_DIRECTORY":
|
|
self.parseImports()
|
|
return self.IMAGE_IMPORT_DIRECTORY
|
|
|
|
elif name == "imports":
|
|
self.parseImports()
|
|
return self.imports
|
|
|
|
elif name == "IMAGE_DELAY_IMPORT_DIRECTORY":
|
|
self.parseDelayImports()
|
|
return self.IMAGE_DELAY_IMPORT_DIRECTORY
|
|
|
|
elif name == "delayImports":
|
|
self.parseDelayImports()
|
|
return self.delayImports
|
|
|
|
elif name == "IMAGE_EXPORT_DIRECTORY":
|
|
self.parseExports()
|
|
return self.IMAGE_EXPORT_DIRECTORY
|
|
|
|
elif name == "forwarders":
|
|
self.parseExports()
|
|
return self.forwarders
|
|
|
|
elif name == "sections":
|
|
self.parseSections()
|
|
return self.sections
|
|
|
|
elif name == "ResourceRoot":
|
|
self.parseResources()
|
|
return self.ResourceRoot
|
|
|
|
elif name == "relocations":
|
|
self.parseRelocations()
|
|
return self.relocations
|
|
|
|
elif name == "IMAGE_LOAD_CONFIG":
|
|
self.parseLoadConfig()
|
|
return self.IMAGE_LOAD_CONFIG
|
|
|
|
elif name == "clr":
|
|
self.parseCLR()
|
|
#return self.clr
|
|
|
|
else:
|
|
raise AttributeError
|
|
|
|
def parseCLR(self):
|
|
self.CLRHeader = None
|
|
self.CLRTables = {}
|
|
self.CLRBlobs = {}
|
|
self.CLRGuids = {}
|
|
self.CLRStrings = {}
|
|
self.CLRUserStrings = {}
|
|
|
|
dirn = self.getDataDirectory(IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR)
|
|
doff = self.rvaToOffset(dirn.VirtualAddress)
|
|
|
|
if doff == 0:
|
|
return None
|
|
|
|
self.IMAGE_COR20_HEADER = clrheader = self.readStructAtOffset(doff, 'pe.IMAGE_COR20_HEADER')
|
|
|
|
# So all the juicy bits live under the Metadata field, but other exports can live under
|
|
# vtable fixups
|
|
|
|
# metadata points to signature header
|
|
# which is followed by storage header
|
|
# which is followed by stream headers
|
|
# which is then followed by different heaps/streams that contain all the data
|
|
metasize = clrheader.Metadata.Size
|
|
moff = metastart = self.rvaToOffset(clrheader.Metadata.VirtualAddress)
|
|
# the clr does some fun things with packing to get things aligned to a 4 byte boundary
|
|
moff += moff % 4
|
|
|
|
self.CLRSignatureHeader = self.readStructAtOffset(moff, 'pe.METADATA_SIGNATURE_HEADER')
|
|
moff += len(self.CLRSignatureHeader)
|
|
moff += fourPad(moff)
|
|
|
|
self.CLRStorageHeader = self.readStructAtOffset(moff, 'pe.METADATA_STORAGE_HEADER')
|
|
moff += len(self.CLRStorageHeader)
|
|
moff += fourPad(moff)
|
|
|
|
shoff = moff
|
|
self.CLRStreamHeaders = []
|
|
# Ultimately there should only be at most 6 streams
|
|
for i in range(self.CLRStorageHeader.NumberOfStreams):
|
|
stream = self.readStructAtOffset(shoff, 'pe.METADATA_STREAM_HEADER')
|
|
shoff += len(stream)
|
|
shoff += fourPad(len(stream))
|
|
self.CLRStreamHeaders.append(stream)
|
|
|
|
# Note: the stream offsets are from the start of the metadata header, not the start
|
|
# of the file
|
|
for sh in self.CLRStreamHeaders:
|
|
name = sh.RCName
|
|
|
|
if name == '#~': # optimized clr data
|
|
self.CLRHeader, self.CLRTables = self.parseOptimizedData(metastart + sh.Offset, sh.Size)
|
|
elif name == '#-': # unoptimized clr data
|
|
self.parseUnoptimizedData(metastart + sh.Offset, sh.Size)
|
|
elif name == '#Strings':
|
|
self.CLRStrings = self.parseStringHeap(metastart + sh.Offset, sh.Size)
|
|
elif name == '#GUID':
|
|
self.CLRGuids = self.parseGuidHeap(metastart + sh.Offset, sh.Size)
|
|
elif name == '#Blob':
|
|
self.CLRBlobs = self.parseBlobHeap(metastart + sh.Offset, sh.Size)
|
|
elif name == '#US':
|
|
'''
|
|
So just up front, the #US heap is actually a blob heap, even though it's
|
|
called the User Strings heap.
|
|
|
|
So the user strings are stored in utf-16 format, with two exceptions. One
|
|
is that all strings have a trailing 1 or 0 byte to indicate whether there
|
|
are any characters with codes greater than 0x7f in the string.
|
|
|
|
Additionally, the #US heap can store *any* binary object since it's just
|
|
a blob heap. So we've gotta take care there. At least since it's a blob
|
|
heap the strings are preceded by their length :|
|
|
'''
|
|
self.CLRUserStrings = self.parseBlobHeap(metastart + sh.Offset, sh.Size)
|
|
else:
|
|
logger.warning('Unhandled CLR stream type of: %s' % name)
|
|
|
|
|
|
def parseOptimizedData(self, offset, size):
|
|
'''
|
|
So I note it down below, it's it's so important I'll note it here as well:
|
|
RIDs are all 1 based, so the first element of most lists are going to be None
|
|
'''
|
|
header = self.readStructAtOffset(offset, 'pe.METADATA_TABLE_STREAM_HEADER')
|
|
ridmask = (2 ** header.Rid) - 1
|
|
|
|
strOffSz = 4 if header.Heaps & 0x1 else 2
|
|
guidOffSz = 4 if header.Heaps & 0x2 else 2
|
|
blobOffSz = 4 if header.Heaps & 0x4 else 2
|
|
|
|
srtd = {}
|
|
tblc = {}
|
|
tables_present = header.MaskValid
|
|
sorted_tables = header.Sorted
|
|
tid = 0
|
|
while tables_present:
|
|
if tables_present & 1:
|
|
tblc[tid] = 0
|
|
tables_present >>= 1
|
|
tid += 1
|
|
|
|
tid = 0
|
|
while sorted_tables:
|
|
if sorted_tables & 1:
|
|
srtd[tid] = 0
|
|
sorted_tables >>= 1
|
|
tid += 1
|
|
|
|
# parse out the table counts. it's just a series of back to back 4 byte integers
|
|
offset += len(header)
|
|
for key in tblc:
|
|
bytez = self.readAtOffset(offset, 4)
|
|
count = struct.unpack("<I", bytez)[0]
|
|
tblc[key] = count
|
|
offset += 4
|
|
|
|
ridbits = len(bin(max(tblc.values()) + 1)[2:])
|
|
ridbytes = 4 if ridbits > 16 else 2
|
|
|
|
ridtbls = {}
|
|
for key, count in tblc.items():
|
|
i = 0
|
|
ctor = clr.RIDTYPEMAP[key]
|
|
# DEV: RIDs are all 1 based indexes :(, so fake it here
|
|
table = [None]
|
|
for i in range(count):
|
|
obj = ctor(tblc, ridlen=ridbytes, slen=strOffSz, glen=guidOffSz, blen=blobOffSz)
|
|
l = len(obj)
|
|
obj.vsParse(self.readAtOffset(offset, l))
|
|
table.append(obj)
|
|
offset += l
|
|
ridtbls[key] = table
|
|
|
|
return header, ridtbls
|
|
|
|
def parseUnoptimizedData(self, offset, size):
|
|
'''
|
|
Now the question is, does this differ at all from optimized in terms of what we need to
|
|
directly parse?
|
|
'''
|
|
header = self.readStructAtOffset(offset, 'pe.METADATA_TABLE_STREAM_HEADER')
|
|
# ridlen = header.Rid % 8 + (1 if header.Rid % 8 > 0 else 0)
|
|
raise NotImplementedError("UNOPT -- TODO")
|
|
|
|
def parseStringHeap(self, offset, size):
|
|
'''
|
|
A string heap isn't the hard coded strings that you'd see in the source
|
|
code. Those live in the #US heap. This is things like class names,
|
|
method names, etc. And it's a true string heap, with strings in utf-8 format
|
|
'''
|
|
strs = {} # indexed by offset in the string table
|
|
data = self.readAtOffset(offset, size)
|
|
consumed = 0
|
|
s = ''
|
|
# The very first and very last values in the returned values will always be
|
|
# \x00 if the table is formatted correctly
|
|
while consumed < size:
|
|
length = data[consumed:].find(b'\x00')
|
|
s = data[consumed:consumed+length]
|
|
strs[consumed] = s.decode('utf-8')
|
|
consumed += len(s) + 1
|
|
# to avoid having duplicate null entries at the end of the dictionary
|
|
if s == '\x00' and consumed > 1:
|
|
break
|
|
return strs
|
|
|
|
def parseBlobHeap(self, offset, size):
|
|
'''
|
|
Blobs in this case are internal binary objects like signatures and others or
|
|
things like users strings, depending on which stream we're looking at.
|
|
|
|
The #US heap is special in that we *technically* can depend on things being
|
|
utf-16. But things can still lie to us about it, so don't really depend on that
|
|
|
|
Structure of a blob in the blob heap is
|
|
<compressed_length><value>
|
|
|
|
where compressed length is their special format
|
|
'''
|
|
blobs = {}
|
|
data = self.readAtOffset(offset, size)
|
|
consumed = 0
|
|
while consumed < size:
|
|
lcomp, objsz = uncompLen(data[consumed:])
|
|
consumed += lcomp
|
|
blobs[consumed] = data[consumed:consumed+objsz]
|
|
consumed += objsz
|
|
return blobs
|
|
|
|
def parseGuidHeap(self, offset, size):
|
|
'''
|
|
Guid heaps are just a series of 16 byte strings immediately following each other.
|
|
There's no delimiting or size parameters. Just a big blob.
|
|
'''
|
|
consumed = 0
|
|
guids = []
|
|
while consumed < size:
|
|
# Is this right? Who references this?
|
|
g = binascii.hexlify(self.readAtOffset(offset + consumed, 16)).decode('utf-8')
|
|
guids.append(g)
|
|
consumed += 16
|
|
|
|
return guids
|
|
|
|
def peFromMemoryObject(memobj, baseaddr):
|
|
fd = vstruct.MemObjFile(memobj, baseaddr)
|
|
return PE(fd, inmem=True)
|
|
|
|
def peFromFileName(fname):
|
|
"""
|
|
Utility helper that assures that the file is opened in
|
|
binary mode which is required for proper functioning.
|
|
"""
|
|
# TODO api change to make context handler
|
|
return PE(open(fname, 'rb'))
|
|
|
|
def peFromBytes(fbytes):
|
|
fd = io.BytesIO(fbytes)
|
|
return PE(fd)
|
|
|