21.1 Python 使用PEfile分析PE文件

PeFile模块是Python中一个强大的便携式第三方PE格式分析工具,用于解析和处理Windows可执行文件。该模块提供了一系列的API接口,使得用户可以通过Python脚本来读取和分析PE文件的结构,包括文件头、节表、导入表、导出表、资源表、重定位表等等。此外,PEfile模块还可以帮助用户进行一些恶意代码分析,比如提取样本中的字符串、获取函数列表、重构导入表、反混淆等等。PEfile模块是Python中处理PE文件的重要工具之一,广泛应用于二进制分析、安全研究和软件逆向工程等领域。

由于该模块为第三方模块,在使用之前读者需要在命令行下执行pip install pefile命令安装第三方库,当安装成功后即可正常使用,如下所示则是该模块的基本使用方法,读者可自行学习理解。

21.1.1 打开并加载PE文件

如下这段代码封装并实现了OpenPeFile函数,可用于打开一个PE文件,在其内部首先判断了可执行文件是否被压缩如果被压缩则会通过zipfile模块将压缩包读入内存并调用C2BIP3函数将数据集转换为2字节,接着再执行pefile.PE()函数,该函数可用于将可执行文件载入,至此读者可在主函数内通过pe.dump_dict()的方式输出该PE文件的所有参数,由于输出的是字典,读者可以使用字典与列表的方式灵活的提取出该程序的所有参数信息。

import sys
import zipfile
import pefile

# 如果是Python3则转换为2字节
def C2BIP3(string):
if sys.version_info[0] > 2:
return bytes([ord(x) for x in string])
else:
return string

# 打开文件
def OpenPeFile(filename):

# 判断是否是ZIP压缩包
if filename.lower().endswith('.zip'):
try:
oZipfile = zipfile.ZipFile(filename, 'r')
file = oZipfile.open(oZipfile.infolist()[0], 'r', C2BIP3('infected'))
except Exception:
print(sys.exc_info()[1])
sys.exit()
oPE = pefile.PE(data=file.read())
file.close()
oZipfile.close()

# 如果是空则
elif filename == '':
oPE = False
return oPE
# 否则直接打开文件
else:
oPE = pefile.PE(filename)
return oPE

if __name__ == "__main__":
pe = OpenPeFile("d://lyshark.exe")
print(pe.FILE_HEADER.dump())
print(pe.dump_dict())

21.1.2 解析PE头部数据

如下代码实现了解析PE结构中头部基本数据,在GetHeader函数内,我们首先通过pe.FILE_HEADER.Machine成员判断当前读入的文件的位数信息,通过pe.FILE_HEADER.Characteristics可判断PE文件的类型,通常为EXE可执行文件或DLL动态链接库文件,通过AddressOfEntryPoint加上ImageBase则可获取到程序的实际装载地址,压缩数据的计算可通过hashlib模块对PE文件字节数据进行计算摘要获取,最后是附加数据,通过get_overlay_data_start_offset则可获取到,并依次循环即可输出所有附加数据。

import hashlib
import pefile

# 计算得到数据长度,自动使用推荐大小
def NumberOfBytesHumanRepresentation(value):
if value <= 1024:
return '%s bytes' % value
elif value < 1024 * 1024:
return '%.1f KB' % (float(value) / 1024.0)
elif value < 1024 * 1024 * 1024:
return '%.1f MB' % (float(value) / 1024.0 / 1024.0)
else:
return '%.1f GB' % (float(value) / 1024.0 / 1024.0 / 1024.0)

# 获取PE头部基本信息
def GetHeader(pe):
raw = pe.write()

# 扫描基本信息
print("-" * 50)
print("程序基本信息")
print("-" * 50)
if (hex(pe.FILE_HEADER.Machine) == "0x14c"):
print("程序位数: {}".format("x86"))
if (hex(pe.FILE_HEADER.Machine) == "0x8664"):
print("程序位数: {}".format("x64"))

if (hex(pe.FILE_HEADER.Characteristics) == "0x102"):
print("程序类型: Executable")
elif (hex(pe.FILE_HEADER.Characteristics) == "0x2102"):
print("程序类型: Dynamic link library")

if pe.OPTIONAL_HEADER.AddressOfEntryPoint:
oep = pe.OPTIONAL_HEADER.AddressOfEntryPoint + pe.OPTIONAL_HEADER.ImageBase
print("实际入口: {}".format(hex(oep)))

print("映像基址: {}".format(hex(pe.OPTIONAL_HEADER.ImageBase)))
print("虚拟入口: {}".format(hex(pe.OPTIONAL_HEADER.AddressOfEntryPoint)))
print("映像大小: {}".format(hex(pe.OPTIONAL_HEADER.SizeOfImage)))
print("区段对齐: {}".format(hex(pe.OPTIONAL_HEADER.SectionAlignment)))
print("文件对齐: {}".format(hex(pe.OPTIONAL_HEADER.FileAlignment)))
print("区块数量: {}".format(int(pe.FILE_HEADER.NumberOfSections + 1)))
print('熵值比例: %f (Min=0.0, Max=8.0)' % pe.sections[0].entropy_H(raw))

# 计算压缩数据
print("-" * 50)
print("计算压缩数据")
print("-" * 50)
print('MD5 : %s' % hashlib.md5(raw).hexdigest())
print('SHA-1 : %s' % hashlib.sha1(raw).hexdigest())
print('SHA-256 : %s' % hashlib.sha256(raw).hexdigest())
print('SHA-512 : %s' % hashlib.sha512(raw).hexdigest())

# 扫描文件末尾是否存在附加数据
print("-" * 50)
print("扫描附加数据")
print("-" * 50)
overlayOffset = pe.get_overlay_data_start_offset()
if overlayOffset != None:
print("起始文件位置: 0x%08x"%overlayOffset)
overlaySize = len(raw[overlayOffset:])
print("长度: 0x%08x %s %.2f%%"%(overlaySize, NumberOfBytesHumanRepresentation(overlaySize), float(overlaySize) / float(len(raw)) * 100.0))
print("MD5: %s" %hashlib.md5(raw[overlayOffset:]).hexdigest())
print("SHA-256: %s" %hashlib.sha256(raw[overlayOffset:]).hexdigest())

if __name__ == "__main__":
pe = pefile.PE("d://lyshark.exe")
GetHeader(pe)

21.1.3 解析节表数据

运用PEFile模块解析节表也很容易,如下代码中分别实现了两个功能函数,函数ScanSection()用于输出当前文件的所有节表数据,其中通过pe.FILE_HEADER.NumberOfSections得到节表数量,并通过循环的方式依次解析pe.sections中的每一个节中元素,函数CheckSection()则可用于计算PE文件节大小以及节MD5值,完整代码如下所示;

import hashlib
import pefile

# 计算得到数据长度,自动使用推荐大小
def NumberOfBytesHumanRepresentation(value):
if value <= 1024:
return '%s bytes' % value
elif value < 1024 * 1024:
return '%.1f KB' % (float(value) / 1024.0)
elif value < 1024 * 1024 * 1024:
return '%.1f MB' % (float(value) / 1024.0 / 1024.0)
else:
return '%.1f GB' % (float(value) / 1024.0 / 1024.0 / 1024.0)

# 输出所有的节
def ScanSection(pe):
print("-" * 100)
print("{:10s}{:10s}{:10s}{:10s}{:10s}{:10s}{:10s}{:10s}".
format("序号","节区名称","虚拟偏移","虚拟大小","实际偏移","实际大小","节区属性","熵值"))
print("-" * 100)
section_count = int(pe.FILE_HEADER.NumberOfSections + 1)

for count,item in zip(range(1,section_count),pe.sections):
print("%d\t\t\t%-10s\t0x%.8X\t0x%.8X\t0x%.8X\t0x%.8X\t0x%.8X\t%f"
%(count,(item.Name).decode("utf-8"),item.VirtualAddress,item.Misc_VirtualSize,item.PointerToRawData,item.SizeOfRawData,item.Characteristics,item.get_entropy()))
print("-" * 100)

# 计算所有节的MD5
def CheckSection(pe):
print("-" * 100)
print("序号\t\t节名称\t\t文件偏移\t\t大小\t\tMD5\t\t\t\t\t\t\t\t\t\t节大小")
print("-" * 100)

# 读取PE文件到内存
image_data = pe.get_memory_mapped_image()

section_count = int(pe.FILE_HEADER.NumberOfSections + 1)
for count,item in zip(range(1,section_count),pe.sections):

section_data = image_data[item.PointerToRawData: item.PointerToRawData + item.SizeOfRawData - 1]
data_size = NumberOfBytesHumanRepresentation(len(section_data))
hash_value = hashlib.md5(section_data).hexdigest()
print("{}\t{:10s}\t{:10X}\t{:10X}\t{:30s}\t{}".format(count,(item.Name).decode("utf-8"),item.PointerToRawData,item.SizeOfRawData,hash_value,data_size))
print("-" * 100)

if __name__ == "__main__":
pe = pefile.PE("d://lyshark.exe")
ScanSection(pe)
CheckSection(pe)

21.1.4 节区RVA与FOA互转

此处计算节偏移地址,相信读者能理解,在之前的文章中我们详细的介绍了PE文件如何进行RVAFOA以及VA之间的转换的,如果是在平时的恶意代码分析中需要快速实现转换那么使用Python将是一个不错的选择,如下代码中RVAToFOA可将一个RVA相对地址转换为FOA文件偏移,FOAToRVA则可实现将一个FOA文件偏移转换为RVA先对地址,当然PeFile模块内也提供了get_rva_from_offset实现从FOA转RVA,get_offset_from_rva则是从RVA到FOA,读者可自行选择不同的转换方式。

import pefile

# 将RVA转换为FOA的函数
def RVAToFOA(pe,rva):
for item in pe.sections:
Section_Start = item.VirtualAddress
Section_Ends = item.VirtualAddress + item.SizeOfRawData
if rva >= Section_Start and rva < Section_Ends:
return rva - item.VirtualAddress + item.PointerToRawData
return -1

# 将FOA文件偏移转换为RVA相对地址
def FOAToRVA(pe,foa):
ImageBase = pe.OPTIONAL_HEADER.ImageBase
NumberOfSectionsCount = pe.FILE_HEADER.NumberOfSections

for index in range(0,NumberOfSectionsCount):
PointerRawStart = pe.sections[index].PointerToRawData
PointerRawEnds = pe.sections[index].PointerToRawData + pe.sections[index].SizeOfRawData

if foa >= PointerRawStart and foa <= PointerRawEnds:
rva = pe.sections[index].VirtualAddress + (foa - pe.sections[index].PointerToRawData)
return rva
return -1

# 内部功能实现FOA->RVA互转
def inside(pe):
# 从FOA获取RVA 传入十进制
rva = pe.get_rva_from_offset(3952)
print("对应内存RVA: {}".format(hex(rva)))

# 从RVA获取FOA 传入十进制
foa = pe.get_offset_from_rva(rva)
print("对应文件FOA: {}".format(foa))

if __name__ == "__main__":
pe = pefile.PE("d://lyshark.exe")
ref = RVAToFOA(pe,4128)
print("RVA转FOA => 输出十进制: {}".format(ref))

ref = FOAToRVA(pe,1056)
print("FOA转RVA => 输出十进制: {}".format(ref))

21.1.5 解析数据为Hex格式

如下代码片段实现了对PE文件的各种十六进制操作功能,封装cDump()类,该类内由多个类函数可以使用,其中HexDump()可用于将读入的PE文件以16进制方式输出,HexAsciiDump()则可用于输出十六进制以及所对应的ASCII格式,GetSectionHex()用于找到PE文件的.text节,并将此节内的数据读入到内存中,这段代码可以很好的实现对PE文件的十六进制输出与解析,读者可在实际开发中使用。

import pefile
from io import StringIO
import sys
import re

dumplinelength = 16

def CIC(expression):
if callable(expression):
return expression()
else:
return expression

def IFF(expression, valueTrue, valueFalse):
if expression:
return CIC(valueTrue)
else:
return CIC(valueFalse)

class cDump():
def __init__(self, data, prefix='', offset=0, dumplinelength=16):
self.data = data
self.prefix = prefix
self.offset = offset
self.dumplinelength = dumplinelength

# 输出指定位置的十六进制格式
def HexDump(self):
oDumpStream = self.cDumpStream(self.prefix)
hexDump = ''
for i, b in enumerate(self.data):
if i % self.dumplinelength == 0 and hexDump != '':
oDumpStream.Addline(hexDump)
hexDump = ''
hexDump += IFF(hexDump == '', '', ' ') + '%02X' % self.C2IIP2(b)
oDumpStream.Addline(hexDump)
return oDumpStream.Content()

def CombineHexAscii(self, hexDump, asciiDump):
if hexDump == '':
return ''
countSpaces = 3 * (self.dumplinelength - len(asciiDump))
if len(asciiDump) <= self.dumplinelength / 2:
countSpaces += 1
return hexDump + ' ' + (' ' * countSpaces) + asciiDump

# 输出指定位置的十六进制格式以及ASCII字符串
def HexAsciiDump(self):
oDumpStream = self.cDumpStream(self.prefix)
hexDump = ''
asciiDump = ''
for i, b in enumerate(self.data):
b = self.C2IIP2(b)
if i % self.dumplinelength == 0:
if hexDump != '':
oDumpStream.Addline(self.CombineHexAscii(hexDump, asciiDump))
hexDump = '%08X:' % (i + self.offset)
asciiDump = ''
if i % self.dumplinelength == self.dumplinelength / 2:
hexDump += ' '
hexDump += ' %02X' % b
asciiDump += IFF(b >= 32 and b <= 128, chr(b), '.')
oDumpStream.Addline(self.CombineHexAscii(hexDump, asciiDump))
return oDumpStream.Content()

class cDumpStream():
def __init__(self, prefix=''):
self.oStringIO = StringIO()
self.prefix = prefix

def Addline(self, line):
if line != '':
self.oStringIO.write(self.prefix + line + '\n')

def Content(self):
return self.oStringIO.getvalue()

@staticmethod
def C2IIP2(data):
if sys.version_info[0] > 2:
return data
else:
return ord(data)

# 只输出十六进制数据
def HexDump(data):
return cDump(data, dumplinelength=dumplinelength).HexDump()

# 输出十六进制与ASCII字符串
def HexAsciiDump(data):
return cDump(data, dumplinelength=dumplinelength).HexAsciiDump()

# 找到指定节并读取hex数据
def GetSectionHex(pe):
ImageBase = pe.OPTIONAL_HEADER.ImageBase
for item in pe.sections:
# 判断是否是.text节
if str(item.Name.decode('UTF-8').strip(b'\x00'.decode())) == ".text":
# print("虚拟地址: 0x%.8X 虚拟大小: 0x%.8X" %(item.VirtualAddress,item.Misc_VirtualSize))
VirtualAddress = item.VirtualAddress
VirtualSize = item.Misc_VirtualSize
ActualOffset = item.PointerToRawData

StartVA = hex(ImageBase + VirtualAddress)
StopVA = hex(ImageBase + VirtualAddress + VirtualSize)
print("[+] 代码段起始地址: {} 结束: {} 实际偏移:{} 长度: {}".format(StartVA, StopVA, ActualOffset, VirtualSize))

# 获取到.text节区间内的数据
hex_code = pe.write()[ActualOffset: VirtualSize]
return hex_code
else:
print("程序中不存在.text节")
return 0
return 0

REGEX_STANDARD = '[\x09\x20-\x7E]'

def ExtractStringsASCII(data):
regex = REGEX_STANDARD + '{%d,}'
return re.findall(regex % 4, data)

def ExtractStringsUNICODE(data):
regex = '((' + REGEX_STANDARD + '\x00){%d,})'
return [foundunicodestring.replace('\x00', '') for foundunicodestring, dummy in re.findall(regex % 4, data)]

# 将传入Hex字符串以每16字符分割在一个列表内
def ExtractStrings(data):
return ExtractStringsASCII(data) + ExtractStringsUNICODE(data)

if __name__ == "__main__":
pe = pefile.PE("d://lyshark.exe")

# 得到.text节内数据
ref = GetSectionHex(pe)

# 转为十六进制格式
dump_hex = HexDump(ref)
print(dump_hex)

# 打包为每16字符一个列表
dump_list = ExtractStrings(dump_hex)

print(dump_list)

21.1.6 解析数据目录表

数据目录表用于记录可执行文件的数据目录项在文件中的位置和大小。数据目录表共有16个条目,每个条目都对应着一个数据目录项,每个数据目录项都描述了可执行文件中某一部分的位置和大小。

数据目录表的解析可以使用pe.OPTIONAL_HEADER.NumberOfRvaAndSizes首先获取到数据目录表的个数,接着二通过循环个数依次解包OPTIONAL_HEADER.DATA_DIRECTORY里面的每一个列表,在循环列表时依次解包输出即可。

import pefile

# 将RVA转换为FOA的函数
def RVAToFOA(pe,rva):
for item in pe.sections:
Section_Start = item.VirtualAddress
Section_Ends = item.VirtualAddress + item.SizeOfRawData
if rva >= Section_Start and rva < Section_Ends:
return rva - item.VirtualAddress + item.PointerToRawData
return -1

# 扫描数据目录表
def ScanOptional(pe):
optional_size = pe.OPTIONAL_HEADER.NumberOfRvaAndSizes
print("数据目录表个数: {}".format(optional_size))

print("-" * 100)
print("编号 \t\t\t 目录RVA\t\t 目录FOA\t\t\t 长度\t\t 描述信息")
print("-" * 100)

for index in range(0,optional_size):
va = int(pe.OPTIONAL_HEADER.DATA_DIRECTORY[index].VirtualAddress)
print("%03d \t\t 0x%08X\t\t 0x%08X\t\t %08d \t\t"
%(index,
pe.OPTIONAL_HEADER.DATA_DIRECTORY[index].VirtualAddress,
RVAToFOA(pe,va),
pe.OPTIONAL_HEADER.DATA_DIRECTORY[index].Size
),end="")

if index == 0:
print("Export symbols")
if index == 1:
print("Import symbols")
if index == 2:
print("Resources")
if index == 3:
print("Exception")
if index == 4:
print("Security")
if index == 5:
print("Base relocation")
if index == 6:
print("Debug")
if index == 7:
print("Copyright string")
if index == 8:
print("Globalptr")
if index == 9:
print("Thread local storage (TLS)")
if index == 10:
print("Load configuration")
if index == 11:
print("Bound Import")
if index == 12:
print("Import Address Table")
if index == 13:
print("Delay Import")
if index == 14:
print("COM descriptor")
if index == 15:
print("NoUse")

if __name__ == "__main__":
pe = pefile.PE("d://lyshark.exe")
ScanOptional(pe)

21.1.7 解析导入导出表

导入表和导出表都是PE文件中的重要数据结构,分别记录着一个模块所导入和导出的函数和数据,如下所示则是使用PeFile模块实现对导入表与导出表的解析工作,对于导入表ScanImport的解析需要通过pe.DIRECTORY_ENTRY_IMPORT获取到完整的导入目录,并通过循环的方式输出x.imports中的数据即可,而对于导出表ScanExport则需要在pe.DIRECTORY_ENTRY_EXPORT.symbols导出符号中解析获取。

import pefile

# 输出所有导入表模块
def ScanImport(pe):
print("-" * 100)
try:
for x in pe.DIRECTORY_ENTRY_IMPORT:
for y in x.imports:
print("[*] 模块名称: %-20s 导入函数: %-14s" %((x.dll).decode("utf-8"),(y.name).decode("utf-8")))
except Exception:
pass
print("-" * 100)

# 输出所有导出表模块
def ScanExport(pe):
print("-" * 100)
try:
for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
print("[*] 导出序号: %-5s 模块地址: %-20s 模块名称: %-15s"
%(exp.ordinal,hex(pe.OPTIONAL_HEADER.ImageBase + exp.address),(exp.name).decode("utf-8")))
except:
pass
print("-" * 100)

if __name__ == "__main__":
pe = pefile.PE("d://lyshark.exe")
ScanImport(pe)
ScanExport(pe)