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 sysimport zipfileimport pefiledef C2BIP3 (string ): if sys.version_info[0 ] > 2 : return bytes ([ord (x) for x in string]) else : return string def OpenPeFile (filename ): 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 hashlibimport pefiledef 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 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 hashlibimport pefiledef 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 ) 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 ) 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文件如何进行RVA
与FOA
以及VA
之间的转换的,如果是在平时的恶意代码分析中需要快速实现转换那么使用Python将是一个不错的选择,如下代码中RVAToFOA
可将一个RVA
相对地址转换为FOA
文件偏移,FOAToRVA
则可实现将一个FOA文件偏移转换为RVA先对地址,当然PeFile模块内也提供了get_rva_from_offset
实现从FOA转RVA,get_offset_from_rva
则是从RVA到FOA,读者可自行选择不同的转换方式。
import pefiledef 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 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 def inside (pe ): rva = pe.get_rva_from_offset(3952 ) print ("对应内存RVA: {}" .format (hex (rva))) 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 pefilefrom io import StringIOimport sysimport redumplinelength = 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 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() def HexAsciiDump (data ): return cDump(data, dumplinelength=dumplinelength).HexAsciiDump() def GetSectionHex (pe ): ImageBase = pe.OPTIONAL_HEADER.ImageBase for item in pe.sections: if str (item.Name.decode('UTF-8' ).strip(b'\x00' .decode())) == ".text" : 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)) 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)] def ExtractStrings (data ): return ExtractStringsASCII(data) + ExtractStringsUNICODE(data) if __name__ == "__main__" : pe = pefile.PE("d://lyshark.exe" ) ref = GetSectionHex(pe) dump_hex = HexDump(ref) print (dump_hex) dump_list = ExtractStrings(dump_hex) print (dump_list)
21.1.6 解析数据目录表 数据目录表用于记录可执行文件的数据目录项在文件中的位置和大小。数据目录表共有16个条目,每个条目都对应着一个数据目录项,每个数据目录项都描述了可执行文件中某一部分的位置和大小。
数据目录表的解析可以使用pe.OPTIONAL_HEADER.NumberOfRvaAndSizes
首先获取到数据目录表的个数,接着二通过循环个数依次解包OPTIONAL_HEADER.DATA_DIRECTORY
里面的每一个列表,在循环列表时依次解包输出即可。
import pefiledef 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 pefiledef 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)