A oto i przykład:
Program ptrzebuje jakiegoś programu aby to zademonstrować, więc po uruchomieniu szuka okna o unikalnym tytule i gdy go nie znajdzie, to je tworzy i uruchamia się od nowa, a gdy znajdzie to dobiera się do procesu właściciela okna.
Dla ułatwienia onkem jest MessageBox, a w nim, w property "callme" jest adres funkcji którą należy wywołać.
Mając uchwyt okna zdobywamy PID i TID (thread id), otwieramy proces i wątek obsługujący okno.
Zatrzymujemy wątek, alokujemy w procesie stronę pamięci i wpisujemy do niej kod, który wyśle na stos parametry i skoczy do podanej funkcji, a następnie wznowi działanie wątku w przerwanym momencie.
Kod pisany w vc++6:
// zanim uruchomisz, zapoznaj się z kolejnością zamykania okien:
- Pierwsze okno będzie zachęcało do zainfekowania procesu.
- Drugie okno się pokaże zaraz po infekcji (ukryta funkcja)
- Trzecie okno będzie prosiło o zamknięcie drugiego okna aby zwolnić pamięć
Ponieważ brak tu jakiejkolwiek automatycznej synchronizacji, zdalnie wywołana funkcja może się wykonywać przez nieokreślony czas (MessageBox), więc jako pierwsze zamykasz okno o tytule "brawo!"
#define _WIN32_WINNT 0xFFFF
#include <windows.h>
#include <stdio.h>
#define CAPTION "1FC90BEF-58F0-4B97-B8FF-AADEEB6B2386" // unikat
void __stdcall TargetFunc(int,int,int);
LRESULT __stdcall SaveFuncLongCbtProc(int,WPARAM,LPARAM);
void CallFunction(DWORD,DWORD,FARPROC,int,int,int);
extern "C" WINBASEAPI HANDLE WINAPI OpenThread(DWORD dwDesiredAccess,BOOL bInheritHandle,DWORD dwThreadId);
HHOOK g_hook;
int main()
{
// bylejaka synchronizacja dla uprzedniego ShellExecute
Sleep(1000);
// okno otwarte?
if (HWND hwnd = FindWindow("#32770", CAPTION))
{
// tak, wywołaj funkcję
DWORD pid;
DWORD thid = GetWindowThreadProcessId(hwnd, &pid);
// adres funkcji do wykonania pobieramy z property okna
FARPROC func = (FARPROC)GetProp(hwnd, "callMe");
CallFunction(pid, thid, func, 3,4,5);
}
else
{
// otwórz okno i uruchom samego siebie
char txt[MAX_PATH];
GetModuleFileName(0, txt, MAX_PATH);
ShellExecute(0,"open",txt,0,0,0);
// stwórz docelowe okno
sprintf(txt, "inject me at 0x%X", (DWORD)TargetFunc);
// zanim otworzymy okno, uswawimy w nim property na adres funkcji TargetFunc()
g_hook = SetWindowsHookEx(WH_CBT, SaveFuncLongCbtProc, 0, GetCurrentThreadId());
MessageBox(0, txt, CAPTION, 0);
UnhookWindowsHookEx(g_hook);
}
return 0;
}
//---------------------------------------------------------
// funkcja którą należy zdalnie wywołać
// jej adres jest wpisywany do MsgBoxa jako property "callMe"
void __stdcall TargetFunc(int q,int w,int e)
{
char txt[64];
sprintf(txt, "funkcja TargetFunc(%d,%d,%d)", q,w,e);
MessageBox(0, txt, "brawo!", MB_TOPMOST);
}
//---------------------------------------------------------
// zapisuje w oknie MsgBox'a adres funkcji do wykonania
LRESULT __stdcall SaveFuncLongCbtProc(int nCode,WPARAM wParam,LPARAM lParam)
{
if (nCode == HCBT_CREATEWND)
SetProp((HWND)wParam, "callMe", (HANDLE)TargetFunc);
return CallNextHookEx(g_hook, nCode, wParam, lParam);
}
//---------------------------------------------------------
//
void CallFunction(DWORD pid,DWORD thid,FARPROC func,int arg1,int arg2,int arg3)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, false, thid);
CONTEXT ctx;
ctx.ContextFlags = 0x1003F;
SuspendThread(hThread);
GetThreadContext(hThread, &ctx);
// zapamiętaj adres aktualnego rozkazu
DWORD dwCurrentEIP = ctx.Eip;
void *remote = VirtualAllocEx(hProcess,0,4096,MEM_COMMIT,PAGE_EXECUTE_READ);
#pragma pack(1)
struct CALLER
{
char x1; // 0x68 push dword
DWORD eip; // dwCurrentEIP
char x2[2]; // 0x60,0x9C pusha;pushf;
char pu3; // 0x68 push dword
int arg3; // arg3
char pu2; // 0x68 push dword
int arg2; // arg2
char pu1; // 0x68 push dword
int arg1; // arg1
char p; // 0xB8 mov eax,
FARPROC fn; // func
char cal[2];// 0xFF,0xD0 call eax
char pop[3];// 0x9D,0x61,0xC2 popf;popa;ret ?
};
#pragma pack()
CALLER code = {
'\x68',dwCurrentEIP, // push dword dwCurrentEIP
'\x60','\x9C', // pusha;pushf;
'\x68',arg3, // push dword arg3
'\x68',arg2, // push dword arg2
'\x68',arg1, // push dword arg1
'\xB8',func, // mov eax,func
'\xFF','\xD0', // call eax
'\x9D','\x61', // popf/popa
'\xC3'}; // ret
WriteProcessMemory(hProcess,remote,&code, sizeof(CALLER), 0);
//wznawiamy wykonywanie wątku w naszej funkcji
ctx.Eip = (DWORD)remote;
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
// ponieważ funkcja 'func' używa MessageBox, zaczekamy
// na jej zakończenie w podobny sposób:
MessageBox(0, "kliknij OK po zamknięciu okna 'brawo'", "", 0);
VirtualFreeEx(hProcess,remote,4096,MEM_RELEASE);
CloseHandle(hThread);
CloseHandle(hProcess);
}
// edit
Mając chwilkę czasu dodałem synchronizację polegającą na automatycznym zwalnianiu pamięci bea potrzeby użycia MessageBox'a.
Użyłem obiektu synchronizacji typu Event, którego uchwyt zostaje zduplikowany dla docelowego procesu. Gdy funkcja func() powróci, event jest ustawiany a program wywołujący tę funkcję jest o tym informowany przez system, po czym odczekuje jeszcze chwilkę i zwalnia pamięć:
(zakładam że kernel32.dll jest w docelowym procesie by default)
// pid - process id
// thid - thread id
// func - adres funkcji do wykonania w procesie pid
// arg* - 4 bajtowe parametry funkcji func
void CallFunction(DWORD pid,DWORD thid,FARPROC func,int arg1,int arg2,int arg3)
{
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, false, thid);
// zamroź wątek
SuspendThread(hThread);
// pobierz rejetry wątku
CONTEXT ctx;
ctx.ContextFlags = 0x1003F;
GetThreadContext(hThread, &ctx);
// zapamiętaj adres aktualnego rozkazu bo zostanie chwilowo podmieniony
DWORD dwCurrentEIP = ctx.Eip;
// zaalokuj stronę pamięci na nasz kod
void *remote = VirtualAllocEx(hProcess, 0, 4096, MEM_COMMIT, PAGE_EXECUTE_READ);
// stwórz synchronizator
HANDLE hEvent = CreateEvent(0, false, false, CAPTION);
#pragma pack(1)
struct CALLER
{
char push1; // push dword dwCurrentEIP <- adres powrotny do przerwanego wątku
DWORD eip;
char paf[2]; // pusha;pushf;
char push2; // push dword arg3
int arg1;
char push3; // push dword arg2
int arg2;
char push4; // push dword arg1
int arg3;
char p; // mov eax, func
FARPROC fn;
char ca1[2]; // call eax // wykonaj func(arg1,arg2,arg3);
char push5; // push dword hEvent // wykonaj SetEvent(hEvent);
HANDLE hEvent;
char x4; // mov eax, SetEvent
FARPROC fn2;
char ca2[2]; // call eax
char push6; // push dword hEvent // wykonaj CloseHandle(hEvent);
HANDLE hEvent2;
char x6; // mov eax, CloseHandle
FARPROC fn3;
char ca3[2]; // call eax
char pop[3]; // popf;popa;ret
};
#pragma pack()
HMODULE kernel32 = GetModuleHandle("kernel32.dll");
#define API(x) GetProcAddress(kernel32, x)
CALLER code = {
'\x68',dwCurrentEIP, // push dword dwCurrentEIP
'\x60','\x9C', // pusha;pushf;
'\x68',arg3, // push dword arg3
'\x68',arg2, // push dword arg2
'\x68',arg1, // push dword arg1
'\xB8',func, // mov eax,func
'\xFF','\xD0', // call eax
'\x68',hEvent, // push dword hEvent
'\xB8',API("SetEvent"),// mov eax,SetEvent
'\xFF','\xD0', // call eax
'\x68',hEvent, // push dword hEvent
'\xB8',API("CloseHandle"),// mov eax,CloseHandle
'\xFF','\xD0', // call eax
'\x9D','\x61', // popf/popa
'\xC3'}; // ret
// zduplikuj event aby inny proces mógł go ustawić
DuplicateHandle(GetCurrentProcess(), hEvent, hProcess, &code.hEvent,DUPLICATE_SAME_ACCESS,false,0);
code.hEvent2 = code.hEvent;
// skopiuj kod do procesu
WriteProcessMemory(hProcess,remote,&code, sizeof(CALLER), 0);
//wznawiamy wykonywanie wątku w naszej funkcji
ctx.Eip = (DWORD)remote;
SetThreadContext(hThread, &ctx);
// uruchom wątek
ResumeThread(hThread);
// czekaj aż funkcja zakończy
WaitForSingleObject(hEvent, INFINITE);
// zaczekaj jeszcze chwilkę aby funkcja zdążyła powrócić do oryginalnego kodu
Sleep(0);
CloseHandle(hEvent);
CloseHandle(hThread);
VirtualFreeEx(hProcess, remote, 4096, MEM_RELEASE);
CloseHandle(hProcess);
}
Małe wyjaśnienie celu użycia rozkazów pusha + pushf:
Funkcja SuspendThread przerywa wykonywanie się wątku i może to się zdarzyć w dowolnym momencie, np. wtedy gdy coś z czymś jest porównywane, a rejestry przechowują pointery do danych, liczniki przebiegów pętli... Jeśli nie zabezpieczymy rejestrów (pusha/popa) to program będzie narażony na wyjątki.
Jeśli nie zabezpieczymy flag procesora (pushf/popf) to wynik "przerwanej" operacji porównania, mnożenia, dodawania... zostanie zafałszowany.
Kod wstrzyknięty w taki sposób powinien spełniać te same wymogi co funkcje obsługi przerwań.
Apropos skoków do funkcji - użycie rejestru jako FARPROC zwalnia nas od obliczania offsetu +- do docelowej funkcji, wiadomo że w dzisiejszych procesorach prawie wszystkie skoki bezpośrednie są relatywne, czyli adres następnego rozkazu + 1,2,4 bajtowy offset (call'a też to dotyczy).