简述
CobaltStrike、MSF生成的原生木马文件,早已被杀毒软件所识别.为了更好的进行上线,一个免杀性很好的shellcode加载器很重要.其中从一个正常的exe文件中获取证书、图标、属性等信息,然后给木马加上这些信息,就可以达到以假乱真,看着就像是正常的exe文件,犹如神来之笔,对于提高免杀性具有很好的作用.
pe文件结构
PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等.一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名.那Windows是怎么区分可执行文件和非可执行文件的呢?其实就是用到PE文件结构来判断的.
PE文件的详细结构如下图所示:
PE文件的简化结构如下图所示:
PE文件头部结构主要分为DOS_HEADER、FILE_HEADER、NT_HEADER32(NT_HEADER64)、OPTIONAL_HEADER32(OPTIONAL_HEADER64)、SECTION_HEADER、DATA_DIRECTORY,各部分的具体结构如下.
IMAGE_DOS_HEADER
字段 | 偏移量 | 长度 | 单位 | 描述 |
---|---|---|---|---|
E_magic | 0x00 | 2 | byte | 文件头标识符 |
E_cblp | 0x02 | 2 | byte | 最后一页字节数 |
E_cp | 0x04 | 2 | byte | 文件页数 |
E_crlc | 0x06 | 2 | byte | 段表中重定位项数 |
E_cparhdr | 0x08 | 2 | byte | 文件头的大小(以段为单位) |
E_minalloc | 0x0A | 2 | byte | 所需的最小附加段数 |
E_maxalloc | 0x0C | 2 | byte | 所需的最大附加段数 |
E_ss | 0x0E | 2 | byte | 初始化时的相对堆栈段地址 |
E_sp | 0x10 | 2 | byte | 初始化时的堆栈段指针 |
E_sum | 0x12 | 2 | byte | 偏移0x00到文件校验和 |
E_ip | 0x14 | 2 | byte | 初始化时的相对代码段地址 |
E_cs | 0x16 | 2 | byte | 初始化时的相对代码段地址 |
E_lfarlc | 0x18 | 2 | byte | 文件中重定位表的文件地址 |
E_ovno | 0x1A | 2 | byte | 覆盖号码 |
E_res | 0x1C | 8 | byte | 保留字 |
E_oemid | 0x24 | 2 | byte | OEM标识符 |
E_oeminfo | 0x26 | 2 | byte | OEM信息 |
E_res2 | 0x28 | 20 | byte | 保留字 |
E_lfanew | 0x3C | 4 | byte | 指向了PE文件的开头(重要) |
IMAGE_FILE_HEADER
字段 | 偏移量 | 长度 | 单位 | 描述 |
---|---|---|---|---|
Machine | 0x00 | 2 | byte | 支持的CPU标识符 |
NumberOfSections | 0x02 | 2 | byte | 指出文件中存在的节区数量 |
TimeDateStamp | 0x04 | 4 | byte | 用来记录编译器创建此文件的时间 |
PointerToSymbolTable | 0x08 | 4 | byte | 指向符号表(用于调试) |
NumberOfSymbols | 0x0C | 4 | byte | 符号表中的符号数量 |
SizeOfOptionalHeader | 0x10 | 2 | byte | 用来指定IMAGE_OPTIONAL_HEADER32或IMAGE_OPTIONAL_HEADER64结构体的长度 |
Characteristics | 0x12 | 2 | byte | 用于标识文件的属性,文件是否时可运行的形态、是否为dll文件等信息,以bit or的形式组合起来 |
IMAGE_NT_HEADER32
字段 | 偏移量 | 长度 | 单位 | 描述 |
---|---|---|---|---|
Signature | 0x00 | 4 | byte | PE文件签名开始标志位 |
FileHeader | 0x04 | 20 | byte | FileHeader结构 |
OptionalHeader | 0x04 | 18 | byte | IMAGE_OPTIONAL_HEADER32结构 |
IMAGE_NT_HEADER64
字段 | 偏移量 | 长度 | 单位 | 描述 |
---|---|---|---|---|
Signature | 0x00 | 4 | byte | PE文件签名开始标志位 |
FileHeader | 0x04 | 20 | byte | FileHeader结构 |
OptionalHeader | 0x04 | 18 | byte | IMAGE_OPTIONAL_HEADER64结构 |
IMAGE_OPTIONAL_HEADER32
字段 | 偏移量 | 长度 | 单位 | 描述 | |
---|---|---|---|---|---|
Magic | 0x00 | 2 | byte | 类型标志位,32位为010B,64位为020B | |
MajorLinkerVersion | 0x02 | 1 | byte | 链接器的主要版本号 | |
MinorLinkerVersion | 0x03 | 1 | byte | 链接器的次要版本号 | |
SizeOfCode | 0x04 | 4 | byte | 代码段的大小 | |
SizeOfInitializedData | 0x08 | 4 | byte | 已初始化数据部分的大小 | |
SizeOfUninitializedData | 0xc | 4 | byte | 未初始化数据部分的大小 | |
AddressOfEntryPoint | 0x10 | 4 | byte | 保存EP(入口点)的RVA(相对虚拟地址).指出程序最先执行的代码起始地址,相当重要. | |
BaseOfCode | 0x14 | 4 | byte | 指向代码段开头的指针,相对于基址 | |
BaseOfData | 0x18 | 4 | byte | 指向数据部分开头的指针,相对于基址 | |
ImageBase | 0x1c | 4 | byte | 加载到内存时程序第一个字节的首选地址,32位系统中进程虚拟内存的范围是0-FFFFFFFF.PE文件被装载到如此大的内存中时,ImageBase指出文件的优先装入地址.装入后,设置EIP=ImageBase+AddressOfEntryPoint | |
SectionAlignment | 0x20 | 4 | byte | 指定节区在内存中的最小单位 | |
FileAlignment | 0x24 | 4 | byte | 指定节区在文件中的最小单位 | |
MajorOperatingSystemVersion | 0x28 | 2 | byte | 操作系统的主要版本号 | |
MinorOperatingSystemVersion | 0x2a | 2 | byte | 操作系统的次要版本号 | |
MajorImageVersion | 0x2c | 2 | byte | 图像的主要版本号 | |
MinorImageVersion | 0x2e | 2 | byte | 图像的次要版本号 | |
MajorSubsystemVersion | 0x30 | 2 | byte | 子系统的主版本号 | |
MinorSubsystemVersion | 0x32 | 2 | byte | 子系统的次要版本号 | |
Win32VersionValue | 0x34 | 4 | byte | 该值保留,必须为0 | |
SizeOfImage | 0x38 | 4 | byte | 文件的大小(以字节为单位),包括所有标头.必须是SectionAlignment的倍数. | Image在虚拟内存中所占空间的大小. |
SizeOfHeaders | 0x3c | 4 | byte | 用来指出整个PE头的大小.该值必须是FileAlignment的整数倍.第一节区所在位置与SizeOfHeaders距文件开始偏移的量相同. | |
CheckSum | 0x40 | 4 | byte | 文件校验和 | |
Subsystem | 0x44 | 2 | byte | 运行此文件所需的子系统 | |
DllCharacteristics | 0x46 | 2 | byte | 映像的DLL特性 | |
SizeOfStackReserve | 0x48 | 4 | byte | 为堆栈保留的字节数 | |
SizeOfStackCommit | 0x4c | 4 | byte | 为堆栈提交的字节数 | |
SizeOfHeapReserve | 0x50 | 4 | byte | 为本地堆保留的字节数 | |
SizeOfHeapCommit | 0x54 | 4 | byte | 为本地堆提交的字节数 | |
LoaderFlags | 0x58 | 4 | byte | 该标志位已过时 | |
NumberOfRvaAndSizes | 0x5c | 4 | byte | 用来指定DataDirectory数组的个数 | |
DataDirectory | 0x60 | 128 | byte | 指向数据目录中第一个IMAGE_DATA_DIRECTORY结构的指针 |
IMAGE_OPTIONAL_HEADER64
字段 | 偏移量 | 长度 | 单位 | 描述 | |
---|---|---|---|---|---|
Magic | 0x00 | 2 | byte | 类型标志位,32位为010B,64位为020B | |
MajorLinkerVersion | 0x02 | 1 | byte | 链接器的主要版本号 | |
MinorLinkerVersion | 0x03 | 1 | byte | 链接器的次要版本号 | |
SizeOfCode | 0x04 | 4 | byte | 代码段的大小 | |
SizeOfInitializedData | 0x08 | 4 | byte | 已初始化数据部分的大小 | |
SizeOfUninitializedData | 0xc | 4 | byte | 未初始化数据部分的大小 | |
AddressOfEntryPoint | 0x10 | 4 | byte | 保存EP(入口点)的RVA(相对虚拟地址).指出程序最先执行的代码起始地址,相当重要. | |
BaseOfCode | 0x14 | 4 | byte | 指向代码段开头的指针,相对于基址 | |
ImageBase | 0x18 | 8 | byte | 加载到内存时程序第一个字节的首选地址,32位系统中进程虚拟内存的范围是0-FFFFFFFF.PE文件被装载到如此大的内存中时,ImageBase指出文件的优先装入地址.装入后,设置EIP=ImageBase+AddressOfEntryPoint | |
SectionAlignment | 0x20 | 4 | byte | 指定节区在内存中的最小单位 | |
FileAlignment | 0x24 | 4 | byte | 指定节区在文件中的最小单位 | |
MajorOperatingSystemVersion | 0x28 | 2 | byte | 操作系统的主要版本号 | |
MinorOperatingSystemVersion | 0x2a | 2 | byte | 操作系统的次要版本号 | |
MajorImageVersion | 0x2c | 2 | byte | 图像的主要版本号 | |
MinorImageVersion | 0x2e | 2 | byte | 图像的次要版本号 | |
MajorSubsystemVersion | 0x30 | 2 | byte | 子系统的主版本号 | |
MinorSubsystemVersion | 0x32 | 2 | byte | 子系统的次要版本号 | |
Win32VersionValue | 0x34 | 4 | byte | 该值保留,必须为0 | |
SizeOfImage | 0x38 | 4 | byte | 文件的大小(以字节为单位),包括所有标头.必须是SectionAlignment的倍数. | Image在虚拟内存中所占空间的大小. |
SizeOfHeaders | 0x3c | 4 | byte | 用来指出整个PE头的大小.该值必须是FileAlignment的整数倍.第一节区所在位置与SizeOfHeaders距文件开始偏移的量相同. | |
CheckSum | 0x40 | 4 | byte | 文件校验和 | |
Subsystem | 0x44 | 2 | byte | 运行此文件所需的子系统 | |
DllCharacteristics | 0x46 | 2 | byte | 映像的DLL特性 | |
SizeOfStackReserve | 0x48 | 8 | byte | 为堆栈保留的字节数 | |
SizeOfStackCommit | 0x50 | 8 | byte | 为堆栈提交的字节数 | |
SizeOfHeapReserve | 0x58 | 8 | byte | 为本地堆保留的字节数 | |
SizeOfHeapCommit | 0x60 | 8 | byte | 为本地堆提交的字节数 | |
LoaderFlags | 0x68 | 4 | byte | 该标志位已过时 | |
NumberOfRvaAndSizes | 0x6c | 4 | byte | 用来指定DataDirectory数组的个数 | |
DataDirectory | 0x70 | 128 | byte | 指向数据目录中第一个IMAGE_DATA_DIRECTORY结构的指针 |
IMAGE_SECTION_HEADER
字段 | 偏移量 | 长度 | 单位 | 描述 | |
---|---|---|---|---|---|
Name | 0x00 | 8 | byte | 节名称 | |
VirtualSize | 0x08 | 4 | byte | 内存中节区所占大小 | |
VirtualAddress | 0xc | 4 | byte | 内存中节区起始地址(RVA) | |
SizeOfRawData | 0x10 | 4 | byte | 磁盘文件中节区所占大小 | |
PointerToRawData | 0x14 | 4 | byte | 磁盘文件中节区起始位置 | |
PointerToRelocations | 0x18 | 4 | byte | 指向分区重定位条目开头的文件指针 | |
PointerToLineNumbers | 0x1c | 4 | byte | 指向节行号条目开头的文件指针 | |
NumberOfRelocations | 0x20 | 2 | byte | 分区的重定位条目数 | |
NumberOfLineNumbers | 0x22 | 2 | byte | 节的行号条目数 | |
Characteristics | 0x24 | 4 | byte | 节区属性(bit | OR) |
IMAGE_DATA_DIRECTORY
字段 | 偏移量 | 长度 | 单位 | 描述 |
---|---|---|---|---|
VirtualAddress | 0x00 | 4 | byte | 虚拟地址 |
Size | 0x04 | 4 | byte | 这个数据结构的大小 |
地址的基本概念
VA(Virtual Address): 虚拟地址 PE 文件映射到内存空间时,数据在内存空间中对应的地址.
RVA(Relative Virtual Address): 相对虚拟地址 PE 文件在内存中的 VA 相对于 ImageBase 的偏移量.
窃取签名
数据目录表
一个数据在文件中或者在内存中的位置基本是固定的,通过数据目录表进行索引是可以找到固定的数据,同一数据目录表中的数据的作用是相同的.
为了方便描述,下文中把含有要窃取签名或资源的PE文件称为源文件,要添加签名或资源的PE文件称为目标文件.
读取数字签名数据
获取签名数据再复制到目标文件相比获取资源数据再复制到目标文件简单多了.从已签名的PE文件的数据目录表中第5个元素读取出VA和Size,然后从VA读取Size大小的数据,这个读取的数据其实就是数字签名数据了.
读取数据后,就需要把数据添加到目标文件了,添加到目标文件中的最后一个节区数据的后面,还要修改目标文件的数据目录表中第5个元素的VA和Size值.
计算Szie
Size就是数字签名数据的长度,这个是和源文件的数据目录表中第5个元素读取的Size一样的.
计算VA
VA是通过目标文件的最后一个SectionHeader的PointerToRawData和SizeOfRawData来计算的,需要注意的是PointerToRawData是按照一定的格式对齐的,即PointerToRawData是fileAlignment的倍数.VA同理,也得是fileAlignment的倍数.
修改文件大小
OPTIONAL_HEADER32(64)中的SizeOfImage表示文件的大小(以字节为单位),由于直接往未签名的文件添加了数字签名数据,所以还需要修改目标文件SizeOfImage的大小,计算公式为SizeOfImage(新) = SizeOfImage(旧) + Size(数字签名数据的长度)
经过读取数字签名数据、计算VA、计算Size、修改文件大小就可以完成从源文件中获取签名并添加到目标文件中.虽然获取到了签名,但是仔细检查的话会发现签名是未验证的,这是因为数字签名数据里记录了PE文件的哈希等信息,并未对数字签名数据进行修改直接复制过来的,所以验证签名必定是失败的.
窃取资源数据
资源数据结构
Windows 将程序的各种界面定义为资源,包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等,资源数据结构如下:
数据目录表中的 IMAGE_DIRECTORY_ENTRY_RESOURCE 条目(第三项)包含资源的 RVA 和大小.资源目录结构中的每一个节点都是由IMAGE_RESOURCE_DIRECTORY 结构和紧跟其后的数个IMAGE_RESOURCE_DIRECTORY_ENTRY 结构组成的.如下图:
获取资源数据再复制PE文件比较复杂,需要计算多次.大概流程分别是添加一个SectionHeader、修改SectionHeader的数量、读取.rsrc节的数据、写入.rsrc节的数据、修改.rsrc节数据中RESOURCE_DATA_ENTRY ResourceDataEntry的DataRVA值、修改 数据目录表中第3个元素的VA和Size值、修改文件的大小
添加一个SectionHeader
SectionHeader的结构在上文介绍PE文件结构时,已经写过了.需要计算结构中各个字段的值并构造一个完整的结构体.直接添加到SectionHeader数组最后的位置,计算量是最小的,也可以插入到SectionHeader数组任意的位置,就是插入位置后面所有的SectionHeader的VirtualAddress、PointerToRawData都要重新计算下.
修改SectionHeader的数量
IMAGE_FILE_HEADER的NumberOfSections表示PE文件里面有多少了节区.由于我们增加了一个,所以目标文件的NumberOfSections要加1.
读取.rsrc节的数据
要把源文件里面.rsrc节的数据读出来,从.rsrc节头PointerToRawData的值开始读取,读取的长度为.rsrc节头SizeOfRawData
写入.rsrc节的数据
读取完.rsrc节的数据后就要插入到目标文件里面,插入的位置就是上面新添加的SectionHeaderde的PointerToRawData.
修改.rsrc节数据中RESOURCE_DATA_ENTRY ResourceDataEntry的DataRVA值
插入.rsrc节的数据后,还需要对数据进行一些改动.要重新计算下RESOURCE_DATA_ENTRY ResourceDataEntry中的DataRVA值.因为有多个资源ID,所以需要把所有的RESOURCE_DATA_ENTRY ResourceDataEntry中的DataRVA值重新计算下.
首先要计算出该资源ID的相对于源文件.rsrc SectionHeader中VA的偏移量是多少,然后用新添加的.rsrc SectionHeader的VA加上这个偏移量,就是新的DataRVA
修改数据目录表中第3个元素的VA和Size值
和添加签名类似,也需要修改目标文件数据目录表的数据,不过我们要修改的是第3个元素,不是第5个元素.VA就是新添加的SectionHeaderde中的VA,Size就是新添加的SectionHeader中的VirtualSize
修改文件大小
OPTIONAL_HEADER32(64)中的SizeOfImage表示文件的大小(以字节为单位),由于直接往目标文件添加了一个SectionHeader和.rsrc节的数据,所以还需要修改SizeOfImage的大小,计算公式为SizeOfImage(新) = SizeOfImage(旧) + .rsrc节的数据的长度 + 40(SectionHeader的长度,固定为40字节)
经过添加一个SectionHeader、修改SectionHeader的数量、读取.rsrc节的数据、写入.rsrc节的数据、修改.rsrc节数据中RESOURCE_DATA_ENTRY ResourceDataEntry的DataRVA值、修改数据目录表中第3个元素的VA和Size值、修改文件的大小这些流程就可以完整的把窃取资源数据窃取出来,还能正常显示了.
总结
经过给生成的木马添加证书、属性、图标等信息后,生成的木马有了很好的伪装,免杀性明显提高.