一、前言
对于感染型病毒而言,如果对同一个目标文件多次进行感染,有可能导致目标文件损坏,使得无法执行。所以病毒程序往往会在第一次感染时对目标文件写进一个感染标志,这样在第二次遇到该文件时,首先判断一下该文件中是否包含有感染标志,如果有,则不再感染,如果没有感染标志则进行感染(关于文件的感染,可参见《反病毒攻防研究第004篇:利用缝隙实现代码的植入》和《反病毒攻防研究第005篇:添加节区实现代码的植入》)。所谓的感染标志其实就是在PE文件中无关紧要的位置写入的一个字符串,所以感染标志的添加、读取与判断操作,其实就是基本的文件读写操作。
二、感染标志的添加
在PE文件结构中存在着许多不实用的字段,比如在IMAGE_DOS_HEADER中,只有e_magic与e_lfanew这两个字段才是重要的,前者用于验证本文件是否为PE文件,后者保存着PE文件的偏移位置。因此可以从第二个字段,即e_cblp(Bytes on last page of file)开始,写入我们的感染标志。这里将该标志设定为“Hack”这四个字符。需要注意的是,将感染标志添加到文件中,应当首先将“Hack”转化为十六进制数值,然后再反向写入(小端显示),代码如下:
#define VIRUSFLAG 0x6b636148 // 感染标志,这里为“Hack” // 感染标志的写入。三个参数分别为:欲感染文件的句柄、欲写入感染标志的位置 // 以及感染标志 BOOL AddSig(HANDLE hFile, DWORD dwAddr, DWORD dwSig) { DWORD dwNum = 0; // 在文件中设置读写位置 SetFilePointer(hFile, dwAddr, 0, FILE_BEGIN); // 写入感染标志 if(WriteFile(hFile, &dwSig, sizeof(DWORD), &dwNum, NULL)) { MessageBox(NULL, "感染标志添加成功!", "提示", MB_OK); return TRUE; } else { MessageBox(NULL, "感染标志添加失败!", "提示", MB_OK); return FALSE; } }
然后编写检测感染标志的代码:
// 感染标志检测 BOOL CheckSig(HANDLE hFile, DWORD dwAddr, DWORD dwSig) { DWORD dwSigNum = 0; DWORD dwNum = 0; SetFilePointer(hFile, dwAddr, 0, FILE_BEGIN); ReadFile(hFile, &dwSigNum, sizeof(DWORD), &dwNum, NULL); if(dwSigNum == dwSig) { return TRUE; } return FALSE; }
我们需要令“病毒”程序在每次感染前先调用CheckSig()函数,依据其返回值来判断目标文件是否已经被感染过,然后再决定是否需要进行感染。主函数代码如下:
#include <windows.h> #define FILENAME "helloworld.exe" // 欲添加感染标志的文件名 #define offsetof(struct_t,member) (size_t)&(((struct_t *)0)-> member) int main() { HANDLE hFile = NULL; hFile = CreateFile(FILENAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(CheckSig(hFile, offsetof(IMAGE_DOS_HEADER, e_cblp), VIRUSFLAG)) { MessageBox(NULL, "本文件已经被感染过!", "提示", MB_OK); return -1; } AddSig(hFile, offsetof(IMAGE_DOS_HEADER, e_cblp), VIRUSFLAG); return 0; }
程序将感染标志写入了IMAGE_DOS_HEADER中的e_cblp位置,它不会对程序的执行产生任何影响。这里需要说明的是,程序中我们使用了offsetof()这个宏,它本来是被定义在stddef.h中的,这里我将其拿出来专门定义。这个宏的作用是求某个结构体的特定成员在结构体里面的偏移量,对于本程序来说就是求e_cblp在IMAGE_DOS_HEADER中的偏移,也就是2(因为它之前的e_magic占用了两个字节)。
这里给大家分析一下(size_t)&(((struct_t*)0)-> member)的意义。首先,(struct_t *)0是一个指向struct_t类型(本程序中为IMAGE_DOS_HEADER)的指针,其指针值为 0,所以其作用就是把从地址 0 开始的存储空间映射为一个struct_t类型的对象。((struct_t *)0)-> member是访问类型中的成员member(本程序中为e_cblp),相应地 &((struct_t *)0)->
member) 就是返回这个成员的地址。由于对象的起始地址为 0,所以成员的地址其实就是相对于对象首地址的成员的偏移地址。最后再通过类型转换,转换为 size_t 类型(32位是unsigned int,64位是long unsigned int)。
三、程序测试
为了测试我们的程序,这里依旧使用《反病毒攻防研究第004篇:利用缝隙实现代码的植入》中所编写的“helloworld.exe”程序。将两个程序放在同一个目录下,感染前先用Hex
Editor Neo查看一下“helloworld.exe”程序的DOS头部分:
图1 感染前的DOS头
然后运行本程序,再次查看DOS头部:
图2 感染后的DOS头
可见,我们的感染是成功的。
四、小结
给文件添加感染标志对于文件来说不会产生任何影响,很多感染型病毒都会给目标文件添加感染标志,比如“熊猫烧香”就会在程序中添加“WhBoy”标志。因此当时李俊所编写的“熊猫烧香”病毒专杀工具(李俊版)就是通过检测文件中是否有“WhBoy”标志来判断文件是否被感染,但是这种检测方法却过于粗糙了。