本文所载网络安全技术仅为技术研究与学习之用,严禁用于非法攻击、侵入他人系统等违法违规行为。所有内容均需遵守国家相关法律法规,滥用所产生的一切法律责任,由行为人自行承担,与本文作者、发布者无关。
书接上回,本质上是对上一节的一些补充(
所以标题只加了一个续,由于一点强迫症的因素章节排号新开了一号,因此有希望成为最短的一集(
鄙人技术浅陋,经网上四处观看浏览总结得此文,如有不足希望师傅们多包容⊙﹏⊙
下面正文开始喵
知识点轰炸
欸捧油 一些小的点集中起来直接一个的给
小端序
正文还没真开始(
先说结论:小端序(Little Endian)是:低字节数据存放在内存的低地址处,高字节数据存放在内存的高地址处
那有小,就得有大吧,那大端序(Big Endian):高字节存低地址,低字节存高地址
从直觉上大端序才是更符合咋们人类的阅读习惯的,for example:
一串数据 0x0000357DF5,在内存中用不同方式存:
- 大端序:00 00 35 7D F5
- 小端序:F5 7D 35 00 00
区别就在于16进制数的位数是:从右往左,为从低位到高位,而内存是从第一个0偏移量的基地址开始最低,往后越来越大,所以小端序的规律虽然是低的在低,高的在高,但实际上是一个个反着存放的
在 x86 (32位) 和 x64 (64位) 中内存都统一使用小端序存放数据
偏移量和基地址
小端序的最后提到了这么个概念,稍微展开说一下
其实对照到物理里面的概念就瞬间好懂了,就是初始位置和位移
引入:RVA(Relative Virtual Address,相对虚拟地址):相对于映像基址的偏移地址,其中映像基址,就是PE文件被载入内存后的起始地址,也就是说实际地址 = 相对虚拟地址 + 映像基址
就像是末状态坐标等于初状态+位移
虚拟地址
上一段提到的一个概念,感觉一层层的都有新东西能说(
虚拟地址和物理地址的区别就是,虚拟地址是程序认为自己所在的地址,虚拟地址技术让程序认为自己占有了一片连续的内存地址空间,基地址都可以从 0x00000000 开始,而实际上真正的物理内存,同时被许许多多的进程动态的占用着
再往深处讲就是内存的页表、缺页处理还有 MMU 了,操作系统相关的教科书或者课程都有讲,也是大学中的操作系统课程必含内容,此处不再多赘述(
书接上集,PE 文件结构从上到下依次是:DOS Header -> DOS Stub -> Rich Header -> NT Headers -> Section Table -> Section1 Section2 … SectionN,这回咱给您说说这个 NT Headers 展开的一些内容
NT Headers
上文说到这段包含3部分:
- Image File Signature(镜像文件签名):一个4字节的内容,标识这是一个PE文件
- Image File Header(镜像文件头):是标准的 COFF 格式,本质是一个结构体,里面包含PE文件相关信息
- Image Optional Header(镜像可选头)这部分叫可选头,是因为像 .obj 这样的对象文件没有这部分,但像 exe 这样的可执行文件必须得有这部分,它可以说是 PE 文件结构中最重要的部分
NT Headers 和 DOS Headers 一样都是结构体:
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;
一样,也是在 winnt.h 进行实现,后缀有 64 的是针对64位的,没有的针对 32 位的
看一下里面的 3 个属性,就是对应上文说的三个部分,第一个是无符号整型,后两个是结构体类型
Image File Signature
结构体中的变量名是Signature,DWORD 类型(Windows 编程涉及到的内容) 表示它是一个 4 字节无符号整型
属性的值是一个固定值:0x00004550,上文说到 x86 架构使用小端序,所以在内存中的顺序为 50 45 00 00,转换成ASCII是P E \0 \0
Image File Header
IMAGE_FILE_HEADER类型的结构体,结构体定义就在 winnt.h:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
- Machine:这字段表示 PE 是运行在 x86 还是 x64上: 值为 0x014C 表示 x86,值为 0x8664 表示 x64
- NumberOfSections:表示节的数量(节的数量也就是节头的数量)
- TimeDateStamp:一个unix时间戳,标注文件被创建的时间
- PointerToSymbolTable 和 NumberOfSymbols:当有 COFF 符号表的时候,PointerToSymbolTable 表示相对 COFF 符号表的文件偏移,NumberOfSymbols 表示符号表中项的数量,但是由于不鼓励使用 COFF 调试信息,所以没有 COFF 符号表,这两项通常为0
- SizeOfOptionalHeader:表示PE Optional Header的大小
- Characteristics:文件属性的标志,文件可以是可执行文件,也可以是系统文件等等
Image Optional Header
上文说过,之所以叫可选头,是因为像 .obj 这样的对象文件没有这部分,但像 exe 这样的可执行文件必须得有这部分,它可以说是 PE 文件结构中最重要的部分
这个就比较特殊了,没有固定的大小,从上段的 Image File Header 那个 结构体里面的 SizeOfOptionalHeader 去获取大小
然后是定义:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
从最开始的 NT headers 的结构体属性中就能看出来,32 位和 64 位版本的不同就是这个的不同
结构体的前 8 个属性:
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
COFF 文件格式的标准属性,其他的是专门给 PE 的
上文说过,32位 PE 和 64 位 PE 差别就在于 Optional Header:
- 个数:32位 OptionalHeader 有 31 个属性,64位版本的有 30 个成员,32位的里面中多出来的一个是 BaseOfData,DWORD类型,表示相对于 data 节的 RVA
- 一些属性的数据类型:有 5 个在 32 位 OptionalHeader 中是 DWORD 类型,64位版本中就是 ULONGLONG 类型,分别是:ImageBase、SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit
上面说的 data 节就是 .data 节
还有 .text .rdata .data…… 等等很多节,这些节的熵值对于免杀和 AV 还有 EDR 的初步静态检测具有一定意义,后面的其他集里面细说
与 PE 结构对应的就是 Section Table 后面的 Section 们,.data 就是其中一个
具体每个属性的含义……我实在懒得自己组织语言了,从网络上摘抄了一篇分析这部分结构体的文章的段落:
- Magic:这个字段表示当前的PE文件是一个32位PE,还是一个64位PE,值为0x10B表示这是一个32位PE,值为0x20B表示这是一个64位PE,IMAGE_FILE_HEADER.Machine是被Windows PE Loader忽略的
- MajorLinkerVersion和MinorLinkerVersion:这两个字段表示链接器的主版本号和次版本号
- SizeOfCode:这个字段表示text段(代码段)的大小,如果有多个代码段,表示全部代码段大小的总和
- SizeOfInitializedData:这个字段表示data段(初始化数据段)的大小,如果有多个初始化数据段,表示全部大小的总和
- SizeOfUninitializedData:这个字段表示bss段(未初始化数据段)的大小,如果有多个未初始化数据段,表示全部大小的总和
- AddressOfEntryPoint:程序被载入内存时入口点的RVA,当载入PE文件时,入口点是程序起始地址的RVA,当载入驱动程序时,入口点是初始化函数的RVA,对于DLL来说,入口点是可选的,当没有入口时,这个字段的值为0
- BaseOfCode:PE文件被载入内存后,text段起始地址的RVA
- BaseOfData:只存在32位PE中,PE文件被载入内存后,data段起始地址的RVA
- ImageBase:这个字段表示PE文件被载入内存后第一个字节的地址,这个地址必须是64K的整数倍,实际上由于KASLR等内存保护措施,几乎不会使用这个地址作为PE的内存基址,PE Loader会选择一个未使用的内存地址作为PE的内存基址,然后开始地址修复,修改所有使用这个地址作为常量的地址,这个过程也叫重定位,这些常量位于一个特殊的节,称为reloc节(重定位节)
- SectionAlignment:这个字段表示内存中节对齐的最小单位,默认值是特定架构(x86或x86-64)的内存页大小,微软规定它的值不能小于FileAlignment
- FileAlignment:这个字段表示文件中节对齐的最小单位,不足的填充0,微软规定它的值应该是512到64 * 1024之间2的整次幂,如果SectionAlignment的值小于内存页的大小,那么FileAlignment的值和SectionAlignment的值必须一致
- MajorOperatingSystemVersion、MinorOperatingSystemVersion、MajorImageVersion、MinorImageVersion、MajorSubsystemVersion、MinorSubsystemVersion:MajorOperatingSystemVersion和MinorOperatingSystemVersion指定了所需操作系统的主版本号和次版本号,后四个字段指定了PE文件的主版本号和次版本号,子系统的主版本号和次版本号
- Win32VersionValue:一个保留字段,值为0
- SizeOfImage:这个字段表示PE文件的大小(包含全部的头),单位是字节,当PE文件被载入到内存时,PE Loader会使用这个字段的值,所以值需要是SectionAlignment的整数倍
- SizeOfHeaders:这个字段表示全部头的大小,包括从DOS Header的第一个字节到Section Header的最后一个字节
- CheckSum:PE文件的校验值,当PE文件被载入到内存时用到
- Subsystem:这个字段指定了运行当前PE文件需要的Windows子系统(如果有的话),完整的值可以参考:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
- DLLCharacteristics:这个字段指定了PE文件的一些特征,比如是否兼容DEP机制、再比如能否在运行时进行重定位,完整的值可以参考:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
- SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit:这几个字段指定了要保留的栈大小、要提交的栈大小、要保留的堆大小、要提交的堆大小
- LoaderFlags:一个保留的字段,值为0
- NumberOfRvaAndSizes:DataDirectory数组的大小
- DataDirectory:IMAGE_DATA_DIRECTORY数组
好!这一节结束,没想到还是写了那么多hhh
预告
下一集大概率讲一讲导入表,以 IAT 为主要内容的一部分,对于免杀的研究来说,针对导入表的相关操作具有很重要的意义喵
收工睡大觉!
