木马免杀之以假乱真

大方无隅,大器晚成,大音希声,大象无形.

Posted by taomujian on May 4, 2024

简述

CobaltStrike、MSF生成的原生木马文件,早已被杀毒软件所识别.为了更好的进行上线,一个免杀性很好的shellcode加载器很重要.其中从一个正常的exe文件中获取证书、图标、属性等信息,然后给木马加上这些信息,就可以达到以假乱真,看着就像是正常的exe文件,犹如神来之笔,对于提高免杀性具有很好的作用.

pe文件结构

PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等.一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名.那Windows是怎么区分可执行文件和非可执行文件的呢?其实就是用到PE文件结构来判断的.

PE文件的详细结构如下图所示: avatar

PE文件的简化结构如下图所示: avatar

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大小的数据,这个读取的数据其实就是数字签名数据了.

avatar

读取数据后,就需要把数据添加到目标文件了,添加到目标文件中的最后一个节区数据的后面,还要修改目标文件的数据目录表中第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文件的哈希等信息,并未对数字签名数据进行修改直接复制过来的,所以验证签名必定是失败的.

avatar

窃取资源数据

资源数据结构

Windows 将程序的各种界面定义为资源,包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等,资源数据结构如下:

avatar

数据目录表中的 IMAGE_DIRECTORY_ENTRY_RESOURCE 条目(第三项)包含资源的 RVA 和大小.资源目录结构中的每一个节点都是由IMAGE_RESOURCE_DIRECTORY 结构和紧跟其后的数个IMAGE_RESOURCE_DIRECTORY_ENTRY 结构组成的.如下图:

avatar

获取资源数据再复制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值重新计算下.

avatar

首先要计算出该资源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值、修改文件的大小这些流程就可以完整的把窃取资源数据窃取出来,还能正常显示了.

avatar

总结

经过给生成的木马添加证书、属性、图标等信息后,生成的木马有了很好的伪装,免杀性明显提高.