Chapter 5 Windows PE病毒
5.1 基本概念
PE病毒:以Windows PE程序为载体,能寄生于PE文件,或Windows系统的病毒程序。
感染:在尽量不影响目标程序(系统)正常功能的前提下,使其具有病毒自己的功能(感染模块、触发模块、破坏模块等)。
5.2 分类
按照感染目标的类型分类:
- 文件感染:将代码寄生在PE文件中。(传统感染型和捆绑释放型感染)
- 传统感染型:在PE文件中添加病毒代码段与数据段,修改节表等控制结构使程序能够首先执行病毒代码。主体是目标程序。优点:被感染后的程序主体依然是目标程序,不影响目标程序图标,隐蔽性稍好。缺点:对病毒代码的编写要求较高,通常是汇编语言编写,难以成功感染自校验程序。
- 捆绑释放型:将目标程序和病毒程序捆在一起,将目标程序作为数据存储在病毒体内。主体是病毒程序。编写较容易,可使用高级语言编写。
- 系统感染:将代码或程序寄生在Windows操作系统,不针对特定的PE文件。感染途径有:
- 即时通信软件
- U盘和光盘
- 电子邮件
- 网络共享等
5.3 传统文件感染
使用技术
- 重定位:病毒代码目标寄生位置不固定
- API函数自获取:在没有引入函数表的情况下获取需要使用的API函数内存地址
- 目标程序遍历搜索:全盘查找,或者部分盘符查找以感染其他文件
- 感染模块:病毒代码插入位置选择与写入、病毒执行完毕后将控制权移交给正常的程序执行流程
重定位
- 在编译时,有些基于Image Base的指令会将地址固定写死在指令之中,如push 0x401215,这时修改Image Base会使得这条指令的意义丢失,因此需要重定位。在病毒代码编译后而没有植入时,其起始地址很可能不是我们想要病毒代码在HOST文件中的起始地址,需要进行移动。
- 其本质是修正实际地址与预期地址的差异
- 解决方案:
- 逐一硬编码(较为繁琐)
- 病毒代码运行过程中自我重定位
call
指令可以将下一条要执行的指令的地址压入栈,配合pop即可得到下一条指令的地址,以此病毒就可以知道自己的地址是什么。
API函数自获取
- 找到DLL文件的引入函数节,在其中进行遍历查找即可。
- kernel32.dll中的关键API函数:GetProcAddress和LoadLibraryA
- 需要首先获得kernel32.dll文件的基地址,可以硬编码但是很难兼容,主要通过kernel32模块中的相应结构和特征定位
- 获取kernel32.dll中的地址的方法:
- 程序入口代码执行时,栈顶存储的地址
系统打开一个可执行文件时,它会调用Kernel32.dll中的CreateProcess函数,CreateProcess函数在完成应用程序装载后,会先将返回地址压入到堆栈顶端。当该应用程序结束后,会将返回地址弹出放到EIP中,继续执行。这个返回地址显然位于kernel32.dll之中。在此基础上按照内存对齐(一般为0x10000)的值向前遍历直至检测到kernel32.dll的文件头 (搜索较费时且容易产生异常情况) - SEH链末端处理函数
SEH:Structured Exception Handler,异常处理模块,以链表形式存在。在链中查找prev成员等于0xFFFFFFFF(表示已经遍历到链表尾)的EXCEPTION_REGISTER结构,该结构中handler值指向系统异常处理例程,它总是位于KERNEL32模块中。根据这一特性,然后进行向前搜索就可以查找KERNEL32.DLL在内存中的基地址。 - PEB相关数据结构指向各模块地址
TEB:Thread Environment Block,线程环境块,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应一个TEB结构体。
PEB:Process Environment Block,进程环境块,存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。- fs:[0]指向TEB结构,TEB结构中偏移0x30位置保存的是PEB的地址,因此可以从fs:[30h]获得PEB地址。
- 然后通过PEB[0x0c]获得PEB_LDR_DATA数据结构地址(即下面的VOID *DllList指针)
1
2
3
4
5
6
7
8
9
10typedef struct _PEB { // Size: 0x1D8
/*000*/ UCHAR InheritedAddressSpace;
/*001*/ UCHAR ReadImageFileExecOptions;
/*002*/ UCHAR BeingDebugged;
/*003*/ UCHAR SpareBool; // Allocation size
/*004*/ HANDLE Mutant;
/*008*/ HINSTANCE ImageBaseAddress; // Instance
/*00C*/ VOID *DllList;
/*010*/ PPROCESS_PARAMETERS *ProcessParameters;
...- 然后通过从PEB_LDR_DATA[0x1c]获取InInitializationOrderModuleList.Flink地址
1
2
3
4
5
6
7
8
9typedef struct _PEB_LDR_DATA
{
ULONG Length; // +0x00
BOOLEAN Initialized; // +0x04
PVOID SsHandle; // +0x08
LIST_ENTRY InLoadOrderModuleList; // +0x0c
LIST_ENTRY InMemoryOrderModuleList; // +0x14
LIST_ENTRY InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24- 最后在Flink[0x08]中得到KERNEL32.DLL模块的基地址。
- 栈区特定高端地址的数据
- 这种方法只适用于Windows NT操作系统,但这种方法的代码量最小,只有25B。
- 每个执行的线程都有它自己的TEB(线程环境块),该块中存储线程的栈顶的地址,从该地址向下偏移0X1C处的地址肯定位于Kernel32.dll中。则可以通过该地址向低地址以64KB为单位来查找Kernel32.dll的基地址。
- 程序入口代码执行时,栈顶存储的地址
- 获取指定函数内存地址的方法
- 通过Address of Names数组查找函数名,记录索引值
- 在Address of Name Ordinals编号数组中找到这个索引值对应的编号
- 在Address of Functions数组中以编号为索引即可找到指定函数的内存地址
目标程序遍历搜索
- 通常以PE文件的格式(EXE、SCR、DLL等)作为感染目标
- 对目标进行搜索通常使用FindFirstFile和FindNextFile两个API函数
- 可进行递归或非递归遍历
文件感染
- 感染的关键在于:
- 病毒代码能够运行
- 选择位置放入病毒代码并将控制权交由病毒代码
- 原有的正常功能不能被破坏
- 记录原始的程序控制点位置,当病毒代码执行完毕后交回控制权
- 设置感染标记,避免重复感染
- 病毒代码能够运行
- 代码插入位置选择
- 添加新节:在新节中专门存放病毒代码,需要检查节表空间是否足够
- 判断该文件是否是可执行文件(检查MZ和PE标识)
- 判断该文件是否已经被感染(避免重复感染)
- 获取数据目录的个数,经过计算得到节表的起始地址
- 得到最后一个节表的偏移,并在其后写入新节的属性等控制信息
- 在病毒节中写入病毒代码和数据
- 修正文件头信息(节的数量等)
- 碎片式感染:将病毒代码分散插入到节之间的填充部分
- 插入式感染:将病毒代码插入到HOST代码节的中间或前后(可能会导致程序无法运行)
- 伴随式感染:备份HOST程序并用自己的程序替换HOST程序,自己的代码执行完之后再去执行HOST备份程序
- 添加新节:在新节中专门存放病毒代码,需要检查节表空间是否足够
5.4 捆绑式感染
HOST作为数据存放在病毒程序中,执行病毒程序时还原并执行HOST文件。熊猫烧香即属于此类病毒。
优点:编写简单、效率高。可感染自校验程序。
缺点:被感染后的程序主体是病毒程序,易被发现(程序叠加+释放执行),程序图标问题。(需要处理好资源节,熊猫烧香就没有处理好导致暴露)
5.5 系统感染
此类病毒通常作为单独个体,不感染系统中的其他文件。
需要通过自启动获得控制权
- 于计算机启动时启动:BIOS-MBR-DBR-系统内部
- 于系统内部启动:修改注册表键值、于系统中特定位置启动、以配置文件形式启动、修改特定文件以启动
- 利用系统自动播放机制(Autorun.inf)
- inf文件是Winodws操作系统下用来描述设备或文件等数据信息的文件。autorun.inf是一个文本形式的配置文件,我们可以用文本编辑软件进行编辑,它只能位于驱动器的根目录下。这个文件包含了需要自动运行的命令,如改变的驱动器图标、运行的程序文件、可选快捷菜单等内容。相关资料
- 在其他可执行文件中嵌入少量病毒代码
- 替换DLL文件
传播方式:可移动磁盘存储与网络传播
5.6 实验内容
链接命令——设定代码节可写:
link /subsystem:windows /section:.text,rwe mype1.obj
其中的/section:.text,rwe表示.text节可读可写可执行。
手动修改入口点,使两个弹窗变成一个弹窗:
将Address of Entry Point进行修改,跳过弹出第一个弹窗的指令(在实验中应为+0x16)
代码重定位写法
1 | call delta ;这条语句执行之后,堆栈顶端为delta在内存中的真正地址 |
或
1 | call @F |
(这里的@F指的是前面最近的一个@@标号,@B指后面最近的一个@@标号)
kernel32.dll基地址获取代码理解
1 | mov eax,[esp] ;from stack |
这里第一条语句为从栈中获取kernel32.dll中的地址保存到eax中。
之后将dx保存为PE文件中new EXE Header的偏移位置(0x3C),检查dx的值是否小于0x1000。
如果小于,再检查Image Base的值是否等于eax(如果eax指向dll文件头,那么Image Base的值应该等于eax)。若等于,则查找完毕,eax即为kernel32.dll的起始地址。
注意上面代码对eax是逐次减1比较,由于内存对齐机制,这里可以直接按照对齐去查找,能够减少很多循环的次数。
kernel32.dll中函数内存地址的获取
API’s Address = ( API’s Ordinal * 4 ) + AddressOfFunctions’ VA + Kernel32 imagebase