现在的位置: 首页 > 综合 > 正文

病毒编程技术-5

2013年10月18日 ⁄ 综合 ⁄ 共 8851字 ⁄ 字号 评论关闭

网络共享资源也是按树状组织的,非叶节点称为容器(container),对容器需要进一步搜索直到到达叶子节点为止,叶子节点才是共享资源的根路径。共享资源一般分成两种:共享打印设备和共享文件夹。对于网络共享文件的搜索,采用WNetOpenEnum和WNetEnumResource(由mpr.dll导出)进行递归枚举。其函数原型及参数含义请参阅MSDN,使用如下代码enumshare.cpp将显示所有的网络驱动器共享文件夹的路径:
  
  #include
  #include
  #pragma comment(lib,"mpr.lib")
  
  int enum_netshare(LPNETRESOURCE lpnr);
  
  void __cdecl main(int argc,char *argv[])
  {
      enum_netshare(0);
  }
  
  
  int enum_netshare(LPNETRESOURCE lpnr)
  {
      DWORD r, rEnum,usage;
      HANDLE hEnum;
      DWORD cbBuffer = 16384;     
      DWORD cEntries = -1;        
      LPNETRESOURCE lpnrLocal;    // NETRESOURCE数组结构的指针
      DWORD i;
  
   
      r = WNetOpenEnum(RESOURCE_GLOBALNET,    // 范围:所有网络资源
                            RESOURCETYPE_DISK,// 类型:仅枚举可存储介质
                            RESOURCEUSAGE_ALL,// 使用状态:所有
                            lpnr,             // 初次调用时为NULL
                            &hEnum);          // 成功后返回的网络资源句柄
  
      if (r != NO_ERROR) { 
          printf("WNetOpenEnum error..../n");
          return FALSE;
      }
     
      lpnrLocal = (LPNETRESOURCE) malloc(cbBuffer);
      if (lpnrLocal == NULL)
          return FALSE;
   
      do
      { 
          ZeroMemory(lpnrLocal, cbBuffer);   
  
          rEnum = WNetEnumResource(hEnum,            
                                        &cEntries,    // 返回尽可能多的结果
                                        lpnrLocal,    // LPNETRESOURCE
                                        &cbBuffer);   // buffer大小
          if (rEnum == NO_ERROR) {
  
              for(i = 0; i < cEntries; i++) {
                 
                  usage = lpnrLocal[i].dwUsage;
                 
                  if(usage & RESOURCEUSAGE_CONTAINER) {
                     
                      if(!enum_netshare(&lpnrLocal[i]))
                          printf("Errors detected in enum process.../n");               
                  }else{
  
                      // 这里病毒可调用遍历函数遍历该共享文件夹下的所有文件
       // enum_path(lpnrLocal[i].lpRemoteName);
                      printf("find %s --> %s/n",lpnrLocal[i].lpLocalName,
                                                  lpnrLocal[i].lpRemoteName);
                  }
              }
          }else if (rEnum != ERROR_NO_MORE_ITEMS) {
              printf("WNetEnumResource error.../n");
              break;
          }
      }while(rEnum != ERROR_NO_MORE_ITEMS);
  
      free((void*)lpnrLocal);
  
      r = WNetCloseEnum(hEnum);
       
      if(r != NO_ERROR) {
          printf("WNetCloseEnum error..../n");
          return FALSE;
      }
  
      return TRUE;
  }

  遍历开始时WNetOpenEnum第4形参为0,在发现共享容器进行递归调用时候,该参数将为共享容器的NETRESOURCE结构指针。从NETRESOURCE结构中可以找到我们感兴趣的lpRemoteName,该指针不为0则表示是有效的共享容器或共享文件夹。

typedef struct _NETRESOURCE {
  DWORD dwScope;
  DWORD dwType;
  DWORD dwDisplayType;
  DWORD dwUsage;
  LPTSTR lpLocalName;
  LPTSTR lpRemoteName;
  LPTSTR lpComment;
  LPTSTR lpProvider;
} NETRESOURCE;

        在解决了起始目录的问题之后,就可以从这些起始目录开始使用FindFirstFile和FindNextFile开始遍历其下以及其子目录下的所有文件和目录了,遍历方法可采用深度优先或广度优先搜索算法,较常用的还是深度优先算法。具体实现方式可采用递归搜索或非递归搜索两种实现方式。递归搜索需要占用栈空间,有可能造成栈空间耗竭而产生异常,不过在现实应用中这种情况很少出现,而非递归搜索则不存在此问题,但代码实现略复杂。在现实应用中,应用最多的还是递归遍历搜索。搜索时,可指定FindFirstFile的第一形参为*.*以搜索所有文件,根据搜索结果WIN32_FIND_DATA结构的dwFileAttributes成员判断是否为目录,若为目录则需要继续遍历该子目录,根据WIN32_FIND_DATA的cFileName中的文件名成员判断是否具有要感染的文件后缀以采取修改感染动作,以下代码实现了递归搜索某个目录及其下所有子目录的功能:

void enum_path(char *cpath){

    WIN32_FIND_DATA wfd;
    HANDLE hfd;
    char cdir[MAX_PATH];
    char subdir[MAX_PATH];

    int r;

    GetCurrentDirectory(MAX_PATH,cdir);
    SetCurrentDirectory(cpath);

    hfd = FindFirstFile("*.*",&wfd);

    if(hfd!=INVALID_HANDLE_VALUE) {
        do{
            if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
                if(wfd.cFileName[0] != '.') {
                    //合成完整路径名
                    sprintf(subdir,"%s//%s",cpath,wfd.cFileName);  
                    //递归枚举子目录
                    enum_path(subdir);               
                }
            }else{

                printf("%s//%s/n",cpath,wfd.cFileName);
                // 病毒可根据后缀名判断是否要感染相应的文件       
            }
           
        }while(r=FindNextFile(hfd,&wfd),r!=0);   
    }
    SetCurrentDirectory(cdir);
}

         短短20多行C代码就实现了文件遍历的功能,Win32 API的强大功能不仅为开发者提供了便利,同时也为病毒敞开了方便之门。用汇编实现则稍微复杂一些,感兴趣的读者可参阅Elkern中的enum_path部分,原理是一样的,限于篇幅这里不再给出相应的汇编代码。
 非递归搜索不使用堆栈存储相关的信息,而使用显式分配的链表或栈等结构存储相关的信息,应用一个迭代循环完成递归遍历同样的功能,下面是使用链表以栈方式处理子目录列表的一个简单实现:

在以汇编语言实现时,需要自己管理链表以及分配和释放相应的结构,因此较为烦琐,代码量也稍大,因此病毒多采用递归的方式进行搜索。值得注意的是搜索深层次的目录是很费时的,因此大部分病毒为避免CPU占用率过高,搜索一定数量的文件之后,都会调用Sleep休眠一会,以避免被敏感的用户发觉。文件搜索和感染模块通常是以单独的线程运行的,在病毒获得控制权后,创建相应的搜索和感染线程,而将主现成的控制权交给原程序。

* PE文件的修改和感染策略

  既然已经能够搜索磁盘及网络共享文件中的所有文件,要实现寄生,那么自然下一步就是对搜索到的PE文件进行感染了。感染PE的很重要的一个考虑就是将病毒代码写入到PE文件的哪个位置。读写文件一般利用Win32 API CreateFile、CreateFileMapping、MapViewOfFile等API以内存映射文件的方式进行,这样可以避免自己管理缓冲的麻烦,因而为较多病毒所采用。为了能够读写具有只读属性的文件,病毒在操作前首先利用GetFileAttributes获取其属性并保存,然后用SetFileAttributes将文件的属性修改为可写,在感染完毕后再恢复其属性值。
  
  一般说来,有如下几种感染PE文件的方案供选择:
  
  a)添加一个新的节。将病毒代码写入到新的节中,相应修改节表,文件头中文件大小等属性值。由于在PE尾部增加了一个节,因此较容易被用户察觉。在某些情况下,由于原PE头部没有足够的空间存放新增节的节表信息,因此还要对其它数据进行搬移等操作。鉴于上述问题,PE病毒使用该方法的并不多。
  b)附加在最后一个节上。修改最后一个节节表的大小和属性以及文件头中文件大小等属性值。由于越来越多的杀毒软件采用了一种尾部扫描的方式,因此很多病毒还要在病毒代码之后附加随机数据以逃避该种扫描。现代PE病毒大量使用该种方式。
  c)写入到PE文件头部未用空间各个节所保留的空隙之中。PE头部大小一般为1024字节,有5-6个节的普通PE文件实际被占用部分一般仅为600字节左右,尚有400多个字节的剩余空间可以利用。PE文件各个节之间一般都是按照512字节对齐的,但节中的实际数据常常未完全使用全部的512字节,PE文件的对齐设计本来是出于效率的考虑,但其留下的空隙却给病毒留下了栖身之地。这种感染方式感染后原PE文件的总长度可能并不会增加,因此自CIH病毒首次使用该技术以来,备受病毒作者的青睐。
  d)覆盖某些非常用数据。如一般exe文件的重定位表,由于exe一般不需要重定位,因此可以覆盖重定位数据而不会造成问题,为保险起见可将文件头中指示重定位项的DataDirectory数组中的相应项清空,这种方式一般也不会造成被感染文件长度的增加。因此很多病毒也广泛使用该种方法。
  e)压缩某些数据或代码以节约出存放病毒代码的空间,然后将病毒代码写入这些空间,在程序代码运行前病毒首先解压缩相应的数据或代码,然后再将控制权交给原程序。该种方式一般不会增加被感染文件的大小,但需考虑的因素较多,实现起来难度也比较大。用的还不多。
  
  不论何种方式,都涉及到对PE头部相关信息以及节表的相关操作,我们首先研究一下PE的修改,即如何在添加了病毒代码后使得PE文件仍然是合法的PE文件,仍然能够被系统加载器加载执行。
  PE文件的每个节的属性都是由节表中的一个表项描述的,节表紧跟在IMAGE_NT_HEADERS后面,因此从文件偏移0x3C处的双字找到IMAGE_NT_HEADERS的起始偏移,再加上IMAGE_NT_HEADERS的大小(248字节)就定位了节表的起始位置,每个表项是一个IMAGE_SECTION_HEADER结构:
  typedef struct _IMAGE_SECTION_HEADER {
      BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]; // 节的名字
      union {
              DWORD   PhysicalAddress;
              DWORD   VirtualSize;    // 字节计算的实际大小
      } Misc;
      DWORD   VirtualAddress;     // 节的起始虚拟地址
      DWORD   SizeOfRawData;     // 按照文件头FileAlignment
                                                      // 对齐后的大小
      DWORD   PointerToRawData;    // 文件中指向该节起始的偏移
      DWORD   PointerToRelocations;
      DWORD   PointerToLinenumbers;
      WORD    NumberOfRelocations;
      WORD    NumberOfLinenumbers;
      DWORD   Characteristics;     // 节的属性
  } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

        节表项的数目由IMAGE_NT_HEADERS的NumberOfSections成员确定。由节表中的起始虚拟地址以及该节在文件中的位置就可以换算加载后内存虚拟地址和文件中地址之间的映射关系。添加一个节则需要修改该节表数组,在其中增加一个表项,然后相应修改NumberOfSections的数目。值得注意的是,某些PE文件现存节表后面可能紧跟着其它数据,如bound import数据,这时就不能简单地增加一个节表项,需要先移动这些数据并修改相应的结构后才能增加节,否则PE文件将不能正常执行。由于很多病毒是自我修改的,因此节属性通常设置为E000XXXX,表示该节可读写执行,否则就需要在病毒的开始处调用VirtualProtect之类的API动态修改内存页的属性了。由上述节表的定义还可以看到每个节的实际数据都是按照文件头中FileAlignment对齐的,这个大小一般是512,因此每个节可能有不超过512字节的未用空间(SizeOfRawData- VirtualSize),这恰好给病毒以可乘之机,著名的CIH病毒首先采用了这种技术,不过问题是每个节的空隙大小是不定的,因此就需要将病毒代码分成若干部分存放,运行时再通过一段代码组合起来,优点是如果病毒代码较小则无需增加PE的大小,隐蔽性较强。如果所有节的未用空间仍不足以容纳病毒代码,则可新增节或附加到最后一个节上。附加到最后一个节上是比较简单的,只要修改节表中最后一个节的VirtualSize以及按FileAlignment对齐后的SizeOfRawData成员即可。当然在上述所有修改节的情况中,如果改变了文件的大小,都要修正文件头中SizeOfImage这个值的大小,该值是所有节和头按照SectionAlignment对齐后的大小。
  这里有两个问题值得注意,第一问题就是对WFP(Windows File Protection)文件的处理,WFP机制是从Windows 2000开始新增的保护系统文件的机制,若系统发现重要的系统文件被改变,则弹出一个对话框警告用户该文件已被替换。当然有多种方法绕过WFP保护,但对病毒而言,更简单的方法就是不感染在WFP列表中的系统文件。可使用sfc.dll的导出函数SfcIsFileProtected判断一个文件是否在该列表中,该API的第一个参数必须为0,第二个参数是要判断的文件名,若在列表中返回非0值,否则返回0。
  另外一个问题就是关于PE文件的校验。大部分PE文件都不使用文件头中的CheckSum域的校验和值,不过有些PE文件,如关键的系统服务程序文件以及驱动程序文件则该值必须正确,否则系统加载器将拒绝加载。PE头部的CheckSum可以使用Imagehlp.dll的导出函数CheckSumMappedFile计算,也可以在将该域清0后按照如下简单的等价算法计算:
  如果PE文件大小是奇数字节,则以0补足,使之按偶数字节。将PE文件头的CheckSum域清0,然后以两个字节为单位进行adc运算,最后和将该累加和同文件实际大小进行adc运算即得到校验和的值。下面的cal_checksum过程假设esi已经指向PE文件头,文件头部CheckSum域已经被清0,CF标志位已经被复位:
           ;调用示例:
           ;clc
           ;push    pe_fileseize
           ;call   cal_checksum
  cal_checksum:
          adc     bp,word [esi]  ;初始esi指向文件头,ebx中保存的是文件大小
          inc     esi
          inc     esi
          loop    cal_checksum
          mov     ebx,[esp+4]
          adc     ebp,ebx   ;ebp中存放的就是PE的校验和
      ret   4

         除了PE头部的校验和之外,很多程序自身也有校验模块,如Winzip和Winrar的自解压文件,如果被感染,将造成无法正常解压缩。因此对于类似的PE文件,病毒应尽量不予感染。
   Elkern中感染文件修改文件相关的代码在infect.asm中,该病毒首先尽可能地利用PE的头部和节的间隙存储自身代码,若所有间隙仍不足以存放病毒代码,则附加到最后一个节上,限于篇幅相关代码从略,感兴趣的读者请自行参阅。
         其实,完成了上述功能的代码片断就已经是一个简单的病毒了,不管是用汇编语言、C语言或是python语言编写的。但这些远不是病毒技术的全部。在病毒和反病毒对抗的数十年中,伴随着反病毒技术的进步,病毒技术也在不断进步着,Win32下的内存驻留感染技术、抗分析技术、EPO技术、多态技术、变形技术等限于篇幅都还没有介绍,无论如何,那都是下篇的内容了。
* 思考与防范
  病毒技术源自编程实践,但又无所不用其极,包含了相当多的编程技巧,如果我们善于借鉴,其中的很多技巧都可用于解决常见的编程难题。此外知己知彼,才能在病毒出现时冷静沉着应对,分析其机制,找到更好的解决之道。作为用户,了解病毒的机制对于选择合适的反病毒产品和方案也是非常有帮助的。
  防范病毒,从用户角度除了使用杀毒软件定期查毒之外,谨慎地下载或执行未知的程序,提高警觉也是非常重要的。
  病毒已经不再单纯是一种展示高超编程技巧的手段了,而被越来越多的领域赋予了其它的如经济、有时甚至是政治的含义。防范病毒,作为负责的程序员,应首先不编写病毒、传播病毒,一切从我做起。

* 参考文献
[1] The PE file format ,LUEVELSMEYER
[2] Microsoft Portable Executable and Common Object File Format Specification ,Microsoft Corp.
[3] An In-Depth Look into the Win32 Portable Executable File Format , Matt Pietrek
[4] 29A issue7

作者简介
温玉杰,男,现从事网络安全工作。主要研究领域为恶意代码、逆向工程、人工智能、编译理论、底层安全技术等。曾与罗云彬合译《intel汇编语言程序设计》,与人合著《软件加密技术内幕》等。

抱歉!评论已关闭.