用Python生成一个PE文件,主要是为了学习PE格式,因为看很多红队工具的原理都要掌握这个,这篇文章主要是记录调试代码的过程。
主要使用的是Python3。
有关PE的文件结构描述,之前写过一个简短的对每个字段的描述
- 描述pe格式的主要地方是
winnt.h
,其中有一节叫做Image Format
,该节给出了DOS MZ格式和Windows 3.1 NE格式的文件头,之后就是PE文件的内容。在这个文件中几乎能找到所有关于PE文件的数据结构定义、枚举类型、常量定义。 - EXE文件和Dll文件是语义上的,它们使用完全的相同的PE格式,唯一的区别就是用一个字段标识这个是EXE还是DLL。
第一版 PE+ShellCode
照着PE结构的描述,用Python实现了,PE格式每个字段都有对应的大小,用到了struct
库。
一个简要的例子,因为Windows一般字符存储都是小端模式,所以用<
标明,后面的字母代表将数值转换的大小
H
unsigned short 占2byteL
unsigned long 占 4byteQ
unsigned long long 占8byte
然后用msf生成一个shellcode,到时候直接填入代码段
$ msfvenom -a x86 -p windows/exec CMD="calc" -f python [-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload No encoder specified, outputting raw payload Payload size: 189 bytes Final size of python file: 932 bytes buf = b"" buf += b"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b" buf += b"\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7" buf += b"\x4a\x26\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf" buf += b"\x0d\x01\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c" buf += b"\x8b\x4c\x11\x78\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01" buf += b"\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x01\xd6\x31" buf += b"\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03\x7d" buf += b"\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66" buf += b"\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0" buf += b"\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f" buf += b"\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d\x85\xb2\x00" buf += b"\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5" buf += b"\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a" buf += b"\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53" buf += b"\xff\xd5\x63\x61\x6c\x63\x00"
完整代码
# 学习pe的最好方法,就是自己写一个PE文件。这个例子展示了用python生成一个pe文件 import struct import time MZ_MAGIC = 0x5A4D PE_MAGIC = 0x4550 IMAGE_FILE_MACHINE_I386 = 0x014c IMAGE_SCN_MEM_EXECUTE = 0x20000000 # Section is executable. IMAGE_SCN_MEM_READ = 0x40000000 # Section is readable. IMAGE_SCN_MEM_WRITE = 0x80000000 # Section is writeable. IMAGE_SCN_CNT_CODE = 0x00000020 IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040 class DOS_HEADER_32(object): ''' DOS头只关心 magic 和 e_lfanew 位置就行 ''' e_magic = MZ_MAGIC e_cblp, e_cp, e_crlc, e_cparhdr, e_minalloc, e_maxalloc, e_ss, e_sp, \ e_csum, e_ip, e_cs, e_lfarlc, e_ovno, e_res, e_oemid, \ e_oeminfo, e_res2, e_lfanew = [0] * 18 def __init__(self): self.fmt = "<30HL" # 小端模式 30个H(unsigned short 占2byte) 后一个是L(unsigned long 占 4byte) self.e_res = [0, 0, 0, 0] self.e_res2 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] def raw(self): return struct.pack(self.fmt, self.e_magic, self.e_cblp, self.e_cp, self.e_crlc, self.e_cparhdr, self.e_minalloc, self.e_maxalloc, self.e_ss, self.e_sp, self.e_csum, self.e_ip, self.e_cs, self.e_lfarlc, self.e_ovno, self.e_res[0], self.e_res[1], self.e_res[2], self.e_res[3], self.e_oemid, self.e_oeminfo, self.e_res2[0], self.e_res2[1], self.e_res2[2], self.e_res2[3], self.e_res2[4], self.e_res2[5], self.e_res2[6], self.e_res2[7], self.e_res2[8], self.e_res2[9], self.e_lfanew) # e_lfanew是文件偏移 def getPEOffset(self): return self.e_lfanew def getSize(self): ''' DOS头,SIZE:30*2+1*4=64 :return: ''' return struct.calcsize(self.fmt) class IMAGE_NT_HEADER_32(object): def __init__(self): self.Signature = PE_MAGIC self.file_header = self.IMAGE_FILE_HEADER() self.optional_header = self.IMAGE_OPTIONAL_HEADER32() def getSize(self): ''' PE文件头,SIZE:4+20+224=248 :return: ''' return 4 + self.file_header.getSize() + self.optional_header.getSize() def raw(self): return struct.pack("<L", self.Signature) + self.file_header.raw() + self.optional_header.raw() class IMAGE_FILE_HEADER: Machine, \ NumberOfSections, \ TimeDateStamp, \ PointerToSymbolTable, \ NumberOfSymbols, \ SizeOfOptionalHeader, \ Characteristics = IMAGE_FILE_MACHINE_I386, 0, 0, 0, 0, 0, 0 def __init__(self): self.fmt = "<2H3L2H" def getSize(self): ''' PE文件逻辑分布的信息,SIZE:2*2+3*4+2*2=20 :return: ''' return struct.calcsize(self.fmt) def raw(self): return struct.pack(self.fmt, self.Machine, self.NumberOfSections, self.TimeDateStamp, self.PointerToSymbolTable, self.NumberOfSymbols, self.SizeOfOptionalHeader, self.Characteristics) class IMAGE_OPTIONAL_HEADER32: Magic = 0x10b # 32位为0x10B,64位为0x20B,ROM镜像为0x107 MajorLinkerVersion = 0 MinorLinkerVersion = 0 SizeOfCode = 0 # 一般放在“.text”节里。如果有多个代码节的话,它是所有代码节的和。必须是FileAlignment的整数倍,是在文件里的大小。 SizeOfInitializedData = 0 SizeOfUninitializedData = 0 AddressOfEntryPoint = 0 # 代码入口点的偏移量,RVA BaseOfCode = 0 # 代码基址,可执行代码的偏移值,RVA BaseOfData = 0 # 数据基址,已初始化数据的偏移值,RVA ImageBase = 0 # 程序默认装入基地址,提供整个二进制文件包括所有头的优先(线性)载入地址,RVA SectionAlignment = 0 FileAlignment = 0 MajorOperatingSystemVersion = 0 MinorOperatingSystemVersion = 0 MajorImageVersion = 0 MinorImageVersion = 0 MajorSubsystemVersion = 4 MinorSubsystemVersion = 0 Win32VersionValue = 0 SizeOfImage = 0 # 内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。 SizeOfHeaders = 0 # DOS头、PE头、区块表的总大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。 CheckSum = 0 # 映像效验和 Subsystem = 2 # 文件子系统,NT用来识别PE文件属于哪个子系统。 对于大多数Win32程序,只有两类值: Windows GUI 和Windows CUI (控制台)。 DllCharacteristics = 0 SizeOfStackReserve = 0 SizeOfStackCommit = 0 SizeOfHeapReserve = 0 SizeOfHeapCommit = 0 LoaderFlags = 0 NumberOfRvaAndSizes = 0x10 # 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。 -> 00 00 00 10 DATA_DIRECTORY = [] def __init__(self): self.fmt = "<HBB9L6H4L2H6L" def getSize(self): ''' SIZE:19*4+9*2+2*1+16*8=224 :return: ''' selfsize = struct.calcsize(self.fmt) for image_data in self.DATA_DIRECTORY: selfsize += image_data.getSize() return selfsize def raw(self): selfdata = struct.pack(self.fmt, self.Magic, self.MajorLinkerVersion, self.MinorLinkerVersion, self.SizeOfCode, self.SizeOfInitializedData, self.SizeOfUninitializedData, self.AddressOfEntryPoint, self.BaseOfCode, self.BaseOfData, self.ImageBase, self.SectionAlignment, self.FileAlignment, self.MajorOperatingSystemVersion, self.MinorOperatingSystemVersion, self.MajorImageVersion, self.MinorImageVersion, self.MajorSubsystemVersion, self.MinorSubsystemVersion, self.Win32VersionValue, self.SizeOfImage, self.SizeOfHeaders, self.CheckSum, self.Subsystem, self.DllCharacteristics, self.SizeOfStackReserve, self.SizeOfStackCommit, self.SizeOfHeapReserve, self.SizeOfHeapCommit, self.LoaderFlags, self.NumberOfRvaAndSizes) for image_data in self.DATA_DIRECTORY: selfdata += image_data.raw() return selfdata class IMAGE_DATA_DIRECTORY: VirtualAddress = 0 Size = 0 def __init__(self): pass def raw(self): return struct.pack("<2L", self.VirtualAddress, self.Size) def getSize(self): return 0x4 * 2 class Section: def __init__(self): self.fmt = "<LLLLLLHHL" self.Name = "" self.VirtualSize = self.VirtualAddress = self.SizeOfRawData = self.PointerToRawData = \ self.PointerToRelocations = self.PointerToLinenumbers = \ self.NumberOfRelocations = self.NumberOfLinenumbers = \ self.Characteristics = 0 # VirtualSize 被实际使用的区块大小,也可是PhysicalAddress,在可执行文件中,它是内容的大小.在目标文件中,它是内容重定位到的地址; # VirtualAddress 区块的RAV地址(相对虚拟地址)。,节中数据的RVA。 # SizeOfRawData 该块在磁盘中所占的大小,原始数据大小,经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数 # PointerToRawData 该块在磁盘文件中的偏移,文件偏移,这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。 def getSize(self): return struct.calcsize(self.fmt) + 8 def has(self, rva, imagebase=0): return (self.VirtualAddress + imagebase) <= rva < (self.VirtualAddress + self.VirtualSize + imagebase) def hasOffset(self, offset): return self.PointerToRawData <= offset < (self.PointerToRawData + self.VirtualSize) def raw(self): self.Name = (self.Name + "\x00" * (8 - len(self.Name)))[:8] return self.Name.encode() + struct.pack(self.fmt, self.VirtualSize, self.VirtualAddress, self.SizeOfRawData, self.PointerToRawData, self.PointerToRelocations, self.PointerToLinenumbers, self.NumberOfRelocations, self.NumberOfLinenumbers, self.Characteristics) class ImportDescriptor: def __init__(self): self.fmt = "<LLLLL" self.OriginalFirstThunk = self.TimeDateStamp = self.ForwarderChain = self.Name = \ self.FirstThunk = 0 def raw(self): return struct.pack(self.fmt, self.OriginalFirstThunk, self.TimeDateStamp, self.ForwarderChain, self.Name, \ self.FirstThunk) def getSize(self): return struct.calcsize(self.fmt) # typedef struct _IMAGE_THUNK_DATA32 { # union { # DWORD ForwarderString; // PBYTE # DWORD Function; // PDWORD # DWORD Ordinal; # DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME # } u1; # } IMAGE_THUNK_DATA32; class ImageThunkData32: Function = 0 def getSize(self): return 4 def raw(self): return struct.pack("<L", self.Function) class ImageImportByName: def __init__(self): self.fmt = "<H" self.Hint = 0 self.Name = "" def getSize(self): size = len(self.Name) + 3 # 1 for \0 + 2 for Hint if size % 2: size += 1 # Padding return size def raw(self): raw = struct.pack(self.fmt, self.Hint) + self.Name.encode() + b"\x00" if len(raw) % 2: raw += "\0" # padding return raw def align(idx, aligment): return (idx + aligment) & ~(aligment - 1) def dword(v): return struct.pack("<L", v) if __name__ == '__main__': length = 0 mz = DOS_HEADER_32() mz.e_lfanew = mz.getSize() length += mz.getSize() # 设置pe头入口 pe = IMAGE_NT_HEADER_32() pe.file_header.NumberOfSections = 1 # section数量 pe.file_header.TimeDateStamp = int(time.time()) pe.file_header.Characteristics = 1 + 2 + 4 + 256 # refer https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.2 pe.optional_header.AddressOfEntryPoint = 0x1000 pe.optional_header.ImageBase = 0x400000 pe.optional_header.SectionAlignment = 0x1000 pe.optional_header.FileAlignment = 0x200 for i in range(pe.optional_header.NumberOfRvaAndSizes): pe.optional_header.DATA_DIRECTORY.append(pe.IMAGE_DATA_DIRECTORY()) pe.file_header.SizeOfOptionalHeader = pe.optional_header.getSize() length += pe.getSize() # .text section text = Section() text.Name = ".text" text.Characteristics = IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE text.VirtualAddress = 0x1000 # .rdataracteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ length += text.getSize() # pading pe.optional_header.SizeOfHeaders = align(length, pe.optional_header.FileAlignment) padding = (pe.optional_header.SizeOfHeaders - length) * b'\x00' length = pe.optional_header.SizeOfHeaders # 写入text代码 buf = b"" buf += b"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b" buf += b"\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7" buf += b"\x4a\x26\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf" buf += b"\x0d\x01\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c" buf += b"\x8b\x4c\x11\x78\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01" buf += b"\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x01\xd6\x31" buf += b"\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03\x7d" buf += b"\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66" buf += b"\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0" buf += b"\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f" buf += b"\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d\x85\xb2\x00" buf += b"\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5" buf += b"\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a" buf += b"\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53" buf += b"\xff\xd5\x63\x61\x6c\x63\x00" section_text = buf text.VirtualSize = len(section_text) text.SizeOfRawData = align(text.VirtualSize, pe.optional_header.SectionAlignment) text.PointerToRawData = length section_text += b"\x00" * (text.SizeOfRawData - len(section_text)) length += len(section_text) # 最后数据的完善 pe.optional_header.SizeOfImage = align(length, pe.optional_header.SectionAlignment) # // Image大小,内存中整个PE文件的映射的尺寸,可比实际的值大,必须是SectionAlignment的整数倍 # 生成二进制 code = b"" code += mz.raw() code += pe.raw() # 生成section代码 code += text.raw() code += padding # 生成每个section具体代码 code += section_text print(code) with open("test.exe", "wb") as f: f.write(code)
这个是第一版本的代码,详细描述了PE文件的组装流程
- 先初始化
DOS_HEADER_32()
- 再
IMAGE_NT_HEADER_32()
- 再定义
section字段
- 再根据
文件对齐
字段补充0对齐 - 再填写section字段的具体代码
我是直接对text字段填入了shellcode(32位),这样一个PE文件就组装好了。
但是生成出来的文件有几kb太大了。原因是文件的section需要字节对齐
text.SizeOfRawData = align(text.VirtualSize, pe.optional_header.SectionAlignment)
SizeOfRawData 要和SectionAlignment对齐才行,就导致了体积膨胀,修改SectionAlignment的大小竟然就无法运行了。但是看到有其他的程序SectionAlignment是可以设置得很小的
后面无意间用lordPE进行修复PE,发现它自动PE大小缩小并且能够运行了。于是我对比了两个文件找到了原因。
text.SizeOfRawData
是根据文件对齐
的字段来对齐的,我用节对齐
的字段对齐了。第一个
sizeofimage
字段我设置的太小了(代码问题,我是根据文件的长度对齐section的)- 导入表段不用对齐文件长度,这个很神奇,text段对齐就好了,导入表字段看lordPE是直接将后面填充
\x00
的去掉了 (这个后面有加导入表的PE格式)
修改SectionAlignment的大小竟然就无法运行了
这个的主要原因是sizeofimage设置得太小了
最后sizeOfImage修改为
pe.optional_header.SizeOfImage = pe.optional_header.SizeOfHeaders + align(text.SizeOfRawData,pe.optional_header.SectionAlignment) + align(rdata.SizeOfRawData, pe.optional_header.SectionAlignment)
第二版 x86 PE + 导入表
上一个版本使用了shellcode执行命令,这个版本直接通过导入表来调用API
有一个坑,MessageBoxA
需要ansi
编码的字符串,MessageBoxW
需要utf-16
编码的字符串,而python3是utf-8编码,所以对字符串变量处理的时候要转换一下
""..encode("mbcs")
ansi编码.encode("UTF-16")
utf16编码
x86 生成的文件1kb左右 (文件对齐默认是512,我尝试将它改小一些,但是会报错)
因为是根据汇编生成的机器码来的,所以需要去寻找字符串和一些dll的内存地址。我将这部分自动化了。
importer = { "user32.dll": ["MessageBoxA"], "kernel32.dll": ["ExitProcess"] } ConstString = { "title": "这是一个标题", "msg": "这是内容,看到我你就成功了~" }
importer代表要导入的dll和函数,ConstString 代表输入的字符串。这是text字段的代码
section_text = b"" section_text += b"\x6a\x40" section_text += b"\x68" + replaceTable["title"] # cccccc01 后面用作替换 title section_text += b"\x68" + replaceTable["msg"] # cccccc02 后面用作替换 msg section_text += b"\x6a\x00" section_text += b"\xff\x15" + replaceTable["MessageBoxA"] # cccccc03 messagebox地址 # push 40 // style # push title # push text # push 0 // hwnd # call messagebox section_text += b"\x6a\x00" section_text += b"\xff\x15" + replaceTable["ExitProcess"] # cccccc04 exitprocess地址 # push 0 # call exitprocess
最后生成代码会自动对这些地址进行替换。
代码地址:学习pe,用python生成pe文件 (github.com)
第三部 x64 + 导入表
32位的搞定了,再看看64位的,PE结构上的差异就几个地方。主要是header头和导入表,写一个新的结构进去就行。
typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64; typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; // PBYTE ULONGLONG Function; // PDWORD ULONGLONG Ordinal; ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA64; typedef IMAGE_THUNK_DATA64 * PIMAGE_THUNK_DATA64; #include "poppack.h" // Back to 4 byte packing //@[comment("MVI_tracked")] typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
然后x64 的调用约定和call的方式也不一样
x64调用约定
在32位汇编中,我们调用一个API时,采用的是stdcall,它有两个特点:一是所有参数入栈,通过椎栈传递;二是被调用的API负责栈指针(ESP)的恢复,我们在调用MessageBox后不用add esp,14h,因为MessageBox已经恢复过了。
x64 call偏移地址计算
参考
代码地址(x64) https://gist.github.com/boy-hack/dbfef2a3eff6b7b00791f6a9714b8aea
将win64改成True就会生成64位的程序了
代码完成了
- 代码完成了call偏移地址自动计算
- 自动置入字符串,自动计算字符串位置
- 代码基本上只需自定义’文本’,’导入函数’,和调用代码,其他的绝对地址转换会自动实现
End
- 因为用了一些代码自动寻找地址,一度以为可以用python写exe了(定义好导入的函数和常量,text代码段可以自定义之类的)
- 对一些PE字段的定义,区块对齐,地址的转换,程序如何调用dll有了更深的了解
发表评论