Windows PE Pwn学习笔记:基础知识 2021-02-20 07:56:23 Steven Xeldax # 目录 [TOC] ## PE文件结构 ![](/download/d466fea9-9117-484e-a3c5-ce957d26198f.png) ![](/download/b7974f44-3700-4d92-b626-38f18ecc3b65.png) ![](/download/ab18e106-63f3-4bc9-ac90-b1a525e622c7.png) PE头包括DOS头(Dos Header)、DOS存根(Dos Stub)、NT头(Nt Header)和节区头(Section Header) ![](/download/9c5149c9-a80a-497a-bab3-4e14f7f042e2.png) ### 安利一个工具:PELAB #### DOS Header ![](/download/d5c28ffd-dcb9-4e39-877d-e73afb0079f7.png) ``` IMAGE_DOS_HEADER { WORD e_magic; // +0000h - EXE标志,“MZ” WORD e_cblp; // +0002h - 最后(部分)页中的字节数 WORD e_cp; // +0004h - 文件中的全部和部分页数 WORD e_crlc; // +0006h - 重定位表中的指针数 WORD e_cparhdr; // +0008h - 头部尺寸,以段落为单位 WORD e_minalloc; // +000ah - 所需的最小附加段 WORD e_maxalloc; // +000ch - 所需的最大附加段 WORD e_ss; // +000eh - 初始的SS值(相对偏移量) WORD e_sp; // +0010h - 初始的SP值 WORD e_csum; // +0012h - 补码校验值 WORD e_ip; // +0014h - 初始的IP值 WORD e_cs; // +0016h - 初始的CS值 WORD e_lfarlc; // +0018h - 重定位表的字节偏移量 WORD e_ovno; // +001ah - 覆盖号 WORD e_res[4]; // +001ch - 保留字00 WORD e_oemid; // +0024h - OEM标识符 WORD e_oeminfo; // +0026h - OEM信息 WORD e_res2[10]; // +0028h - 保留字 LONG e_lfanew; // +003ch - PE头相对于文件的偏移地址 } ``` DOS头的下面是DOS Stub。整个DOS Stub是一个字节块,其内容随着链接时使用的链接器不同而不同,PE中并没有与之对应的相关结构。 #### NT Header IMAGE_NT_HEADERS是广义上的PE头,在标准的PE文件中其大小为456个字节,由4个字节的PE标识符 + IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER32组成。 ``` IMAGE_NT_HEADERS { DWORD Signature; // +0000h - PE文件标识,“PE00” IMAGE_FILE_HEADER FileHeader; // +0004h - PE标准头 IMAGE_OPTIONAL_HEADER32 OptionalHeader; // +0018h - PE扩展头 } ``` ![](/download/0993a774-4a69-4496-8798-ce71da725f94.png) #### File Header ![](/download/c13d5505-82d1-4889-ae02-b7cb635c50a3.png) 标准PE头IMAGE_FILE_HEADER紧跟在PE头标识后,即位于IMAGE_DOS_HEADER的e_lfanew + 4的位置。由此位置开始的20个字节为数据结构标准PE头IMAGE_FILE_HEADER的内容。它记录了PE文件的全局属性,如该PE文件运行的平台、PE文件类型、文件中存在的节的总数等。 ``` IMAGE_FILE_HEADER { WORD Machine; // +0004h - 运行平台 WORD NumberOfSections; // +0006h - PE中节的数量 DWORD TimeDateStamp; // +0008h - 文件创建日期和时间 DWORD PointerToSymbolTable; // +000ch - 指向符号表 DWORD NumberOfSymbols; // +0010h - 符号表中的符号数量 WORD SizeOfOptionalHeader; // +0014h - 扩展头结构的长度 WORD Characteristics; // +0016h - 文件属性 } ``` #### Option Header 文件执行时的入口地址、文件被操作系统装入内存后的默认基地址,以及节在磁盘和内存中的对齐单位等信息均可以在此结构中找到。对该结构中的某些数值的随意改动可能会造成PE文件的加载或运行失败。 ``` IMAGE_OPTIONAL_HEADER { WORD Magic; // +0018h - 魔术字107h = ROM Image,10bh = exe Image BYTE MajorLinkerVersion; // +001ah - 链接器版本号 BYTE MinorLinkerVersion; // +001bh - DWORD SizeOfCode; // +001ch - 所有含代码的节的总大小 DWORD SizeOfInitializedData; // +0020h - 所有含已初始化数据的节的总大小 DWORD SizeOfUninitializedData; // +0024h - 所有含未初始化数据的节的大小 DWORD AddressOfEntryPoint; // +0028h - 程序执行入口RVA DWORD BaseOfCode; // +002ch - 代码的节的起始RVA DWORD BaseOfData; // +0030h - 数据的节的起始RVA DWORD ImageBase; // +0034h - 程序的建议装载地址 DWORD SectionAlignment; // +0038h - 内存中的节的对齐粒度 DWORD FileAlignment; // +003ch - 文件中的节的对齐粒度 WORD MajorOperatingSystemVersion; // +0040h - 操作系统版本号 WORD MinorOperatingSystemVersion; // +0042h - WORD MajorImageVersion; // +0044h - 该PE的版本号 WORD MinorImageVersion; // +0046h - WORD MajorSubsystemVersion; // +0048h - 所需子系统的版本号 WORD MinorSubsystemVersion; // +004ah - DWORD Win32VersionValue; // +004ch - 未用 DWORD SizeOfImage; // +0050h - 内存中的整个PE映象尺寸 DWORD SizeOfHeaders; // +0054h - 所有头+节表的大小 DWORD CheckSum; // +0058h - 校验和 WORD Subsystem; // +005ch - 文件的子系统 WORD DllCharacteristics; // +005eh - DLL文件特性 DWORD SizeOfStackReserve; // +0060h - 初始化时的栈大小 DWORD SizeOfStackCommit; // +0064h - 初始化时实际提交的栈大小 DWORD SizeOfHeapReserve; // +0068h - 初始化时保留的堆大小 DWORD SizeOfHeapCommit; // +006ch - 初始化时实际提交的堆大小 DWORD LoaderFlags; // +0070h - 与调试有关 DWORD NumberOfRvaAndSizes; // +0074h - 下面的数据目录结构的项目数量 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 0078h - 数据目录 } ``` ![](/download/4e0900f3-66ec-4bac-99bd-9a77c76f7cfa.png) #### Data Directories ![](/download/f6c04f27-c2fb-4cc4-a8f6-d8e9977c2af8.png) IMAGE_OPTIONAL_HEADER结构的最后一个字段为DataDirectory。 该字段定义了PE文件中出现的所有不同类型的数据的目录信息,从Windows NT 3.1操作系统开始到现在,该数据目录中定义的数据类型一直是16种。 PE中使用了一种称作“数据目录项IMAGE_DATA_DIRECTORY”的数据结构来定义每种数据。 ``` IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // +0000h - 数据的起始RVA DWORD Size; // +0004h - 数据块的长度 } ``` 总的数据目录一共由16个相同的IMAGE_DATA_DIRECTORY结构连续排列在一起组成。 如果想在PE文件中寻找特定类型的数据,就需要从该结构开始。 这16个元组的数组每一项均代表PE中的某一个类型的数据,各数据类型为: 数组编号中文描述0导出表地址和大小1导入表地址和大小2资源表地址和大小3异常表地址和大小4属性证书数据地址和大小5基地址重定位表地址和大小6调试信息地址和大小7预留为08指向全局指针寄存器的值9线程局部存储地址和大小10加载配置表地址和大小11绑定导入表地址和大小12导入函数地址表地址和大小13延迟导入表地址和大小14CLR运行时头部数据地址和大小15系统保留 #### Section ![](/download/31168aa7-364a-4899-8b33-9eca83330bab.png) PE头IMAGE_NT_HEADERS后紧跟着节表。它由许多个节表项(IMAGE_SECTION_HEADER)组成,每个节表项记录了PE中与某个特定的节有关的信息,如节的属性、节的大小、在文件和内存中的起始位置等。节表中节的数量由字段IMAGE_FILE_HEADER.NumberOfSection来定义。 ``` IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // +0000h - 8个字节节名 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; // +0008h - 节区的尺寸 DWORD VirtualAddress; // +000ch - 节区的RVA地址 DWORD SizeOfRawData; // +0010h - 在文件中对齐后的尺寸 DWORD PointerToRawData; // +0014h - 在文件中的偏移 DWORD PointerToRelocations; // +0018h - 在OBJ文件中使用 DWORD PointerToLinenumbers; // +001ch - 行号表的位置(供调试用) WORD NumberOfRelocations; // +0020h - 在OBJ文件中使用 WORD NumberOfLinenumbers; // +0022h - 行号表中行号的数量 DWORD Characteristics; // +0024h - 节的属性 } ``` ### 参考资料 https://zhuanlan.zhihu.com/p/31967907 https://blog.csdn.net/liuyez123/article/details/51281905 https://bbs.pediy.com/thread-264227.htm https://bbs.pediy.com/thread-264232.htm https://bbs.pediy.com/thread-265024.htm#msg_header_h2_0 ## SEH Windows采用了链表的方式来构造,在出现异常的时候通过遍历链表,找到第一个能处理异常的SEH来执行。 SEH的结构体如下: ``` typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; //这里指向链表中的下一个SEH EXCEPTION_DISPOSITION (*Handler)( struct _EXCEPTION_RECORD *record, void *frame, struct _CONTEXT *ctx, void *dispctx); } EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD; ``` SEH链表头为fs:[0],fs是一个段寄存器,fs:[0]指向第一个SEH结构体的地址。第一个SEH结构体通过Next指针指向下一个SEH结构体。 在c语言中,我们可以在代码中通过try/exception来自己注册SEH,如下: ``` SEH链表头为fs:[0],fs是一个段寄存器,fs:[0]指向第一个SEH结构体的地址。第一个SEH结构体通过Next指针指向下一个SEH结构体。 在c语言中,我们可以在代码中通过try/exception来自己注册SEH,如下: ``` ![](/download/c989c165-9936-4136-9845-974b0cd6f47e.png) ### 调试 ``` Example: // ConsoleApplication1.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <Windows.h> #include <stdio.h> #include <stdlib.h> void get_print() { char str[11]; __try { scanf(str,"%s"); int b = 0; int a= 1 / b; printf("%s\n", str); } __except (EXCEPTION_EXECUTE_HANDLER) { printf("done"); // } } int main() { get_print(); printf("gogo"); return 0; } ``` 在程序未触发异常时候 ![](/download/d23d98d5-8015-4991-98d4-d41d424c2f3d.png) SEH链如下: ![](/download/7cc16292-efa0-4b24-9459-f87671fd49be.png) 在程序触发异常后 ![](/download/a77e4e78-381d-4756-beb4-381bcd11cc8b.png) ## 导入表导出表 Windows程序没有延迟绑定机制自然也就没有PLT/GOT表,但是Windows程序显然也是要调用所谓的库函数的,Windows下的函数库是DLL文件,类似于Unix下的libc文件,程序调用库函数需要借助的就是导入表和导出表了。 导入表是PE数据组织中的一个很重要的组成部分,它是为实现代码重用而设置的。通过分析导入表数据,可以获得诸如PE文件的指令中调用了多少外来函数,以及这些外来函数都存在于哪些动态链接库里等信息。Windows加载器在运行PE时会将导入表中声明的动态链接库一并加载到进程的地址空间,并修正指令代码中调用的函数地址。在数据目录中一共有四种类型的数据与导入表数据有关: 导入表、导入函数地址表、绑定导入表、延迟加载导入表。 **程序中,导入表的地址通常位于.idata段** http://xiaoxin.zone/2020/08/06/windows-pwn-huan-jing-da-jian-ji-chu-zhi-shi/ https://www.cnblogs.com/yilang/p/11233935.html ## windows程序保护 ### ALSR 与linux相同,ALSR保护指的是地址随机化技术,这项技术将在程序启动时候将dll随机的加载到内存中的位置。注意是DLL 绕过方法: 信息泄露(包括跨进程) 爆破(win7 x64, win10 x86) :64位操作系统的前32位是不变的,后16bit也是不变的,只有两字节会变。 攻击 Non-ASLR 的镜像或 top down alloc(win7) 堆喷定位内存:申请很大的空间去放shellcode,通常大小在200M左右,主要目的是覆盖0x0c0c0c0c。shellcode前也是填充大量的0x0c0c0c0c。0x0c0c这条指令对应的汇编指令是or al,0ch。这条指令与0x90的NOP指令类似。有文章说堆喷要求攻击者能够控制单次申请的堆块的大小,所以基本上只有在支持动态语言脚本(如JavaScript,ActionScript等)的程序中才能使用 ### High Entropy VA 这个保护被称作为高熵64位地址空间布局随机化,一旦开启,表示此程序的地址随机化的取值空间为64bit,这会导致攻击者更难推测随机化后的地址。 ### Force Integrity 这个保护被称为强制签名保护,一旦开启表示此程序加载时候需要验证其中的签名,如果签名不正确,程序将会被阻止运行。 ### Isolation 这个保护被称作为隔离保护,一旦开启,表示次程序加载时候会将一个相对独立的环境中被加载,从而阻止攻击者过度提升权限。 ### NX/DEP/PAE 与linux相同,NX保护指的是内存页不可运行,这个技术能够将内存标记为不可执行,从而防止从该内存区域运行代码。它帮助防止代码在数据页面(例如堆,栈,内存池)中运行。在windows中常常被称为DEP(数据执行保护,Data Execution Prevention),同时引入了一个新的机制被称为PAE(物理地址拓展,Physical Address Extension),PAE是一项处理器功能,使x86处理器可以在部分Windows版本上访问4 GB以上的物理内存. ### SEHOP structured exception handling overwrite protection 结构化异常处理保护,这个保护能够防止攻击者利用结构化异常处理来进一步的利用。 ### CFG 控制流保护,Control Flow Guard,这项技术通过在间接跳转前插入校验代码,检查目标地址的有效空间,进而可以阻止执行流跳转到预期之外的地点,最终及时并有效的进行异常处理,避免引发相关的安全问题。 所有的直接call都会被CFG检查,有一个预先设置的read-only的bitmap。 编译器将间接函数调用的目的地址保存在GuardCFFunction Table中。在执行间接调用函数之前,会检查跳转的这些地址是否存在于表里 ### RFG 返回地址保护,Return Flow Guard,这项技术会在每个函数头部将返回地址保存到fs:[rsp],thread control stack里面,并在函数返回前将其与栈上的返回地址进行比较,从而有效的阻止了针对返回地址的攻击。 ### SafeSEH 安全结构化异常处理,这项技术可以理解为一个白名单版的安全沙箱,它会事先为你定义一些异常处理程序,并基于此构造安全结构化异常处理表,程序正式运行后,安全结构化异常处理表之外的异常处理程序被阻止运行。 ### GS 这个保护类似于linux中的canary保护,一旦开启,会在返回地址和BP之前压入一个额外的security cookie。系统会比较栈中的这个值和原先放在.data中的值进行一个比较,如果两者不吻合,说明栈中发生了溢出。 ### Authenticode 签名保护 ### .NET DLL混淆级保护 https://ble55ing.github.io/2019/08/18/WindowsPwn0/#gs