Приветствую. В этой статье я постараюсь объяснить принципы исполнения кода в чужом процессе. В качестве примера я представлю перехват API путём изменения начальных байт перехватываемой процедуры.
Для начала необходимо посмотреть, как будет выглядеть код, перехватывающий
API при обычных условиях, то есть в теле DLL-библиотеки. Находим реальный адрес входа в функцию. Перезаписываем по этому адресу дальний джамп к нашему обработчику. Вот и всё что нам нужно для перехвата API. Как это выглядит в коде смотрите ниже.

pTemp:=GetProcAddress(GetModuleHandle(LibName), InterceptedProcName);
ReadProcessMemroy(GetCurrentProcess, pTemp, @instr, 2, bw);
ReadProcessMemroy(GetCurrentProcess, Pointer(DWORD(pTemp)+2), @pto, 4, bw);
if instr=$25FF then
pTemp:=Pointer(pto);
buf.PushOp:=$68;
buf.PushArg:=DWORD(pNewProc);
buf.RetOp:=$C3;
WriteProcessMemory(GetCurrentProcess, pTemp, @buf, 6, bw);
FreeLibrary(hKernelLib);

Buf – это данные, перезаписывающие начальные 6 байт процедуры. Он имеет такой формат:

far_jmp=packed record
PuhsOp: byte;
PushArg: DWORD;
RetOp: byte;
end;

Заполняя его, мы создаём как бы такой ассемблерный код:

push PuhsArg ; В данном случае тут лежит указатель на новую процедуру
retf ;
возврат из кода

Теперь нам надо разобрать механизм внедрения потока в чужой процесс. Внедрять поток (thread) мы будем так:
1) Записываем в буфер код нашего внедряемого потока.
2) Выделяем память под этот буфер в удалённом процессе.
3) Записываем в этот участок памяти код процедуры.
4) Создаём структуру, заполняя которую мы можем создать код передающий управление на наш записанный поток.
В коде это выглядит вот так:

type
TCTC2=packed record
CTCO: WORD;
CTCA: DWORD;
ExitThreadPushOp: byte;
ExitThreadPushArg: dword;
ExitThreadCallOp: word;
ExitThreadCallArg: dword;
ExitThreadAddress: dword;
CTAddress: DWORD;
end;
var
CTC: TCTC2;
pCode: Pointer;
pRemThread: DWORD;
hProcess, hThread: integer;
bw: Cardinal;
fCode: array of byte;
TargetPID: Cardinal;
hSnapshoot: integer;
pe32: TProcessEntry32;
begin
hSnapshoot:=CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0);
pe32.dwSize:=SizeOf(TProcessEntry32);
Process32First(hSnapshoot, pe32);
repeat
if (pe32.szExeFile='Project2.exe') then
TargetPID:=pe32.th32ProcessID;
until (not Process32Next(hSnapshoot,pe32));

hProcess:=OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION,
false, TargetPID);
pRemThread:=DWORD(VirtualAllocEx(hProcess, nil, DWORD(@RemThreadEnd)-DWORD(@RemThread),
MEM_COMMIT, PAGE_EXECUTE_READWRITE));
SetLength(fCode, DWORD(@RemThreadEnd) - DWORD(@RemThread));
CopyMemory(fCode, @RemThread, length(fCode));
WriteProcessMemory(hProcess, Pointer(pRemThread), fCode, length(fCode), bw);

pCode:=VirtualAllocEx(hProcess, nil, sizeof(CTC), MEM_COMMIT, PAGE_EXECUTE_READWRITE);

CTC.CTCO:=OPCODE_CALL;
CTC.CTCA:=DWORD(pCode)+ DWORD(@CTC.CTAddress)- DWORD(@CTC);
CTC.ExitThreadPushOp:=OPCODE_PUSH;
CTC.ExitThreadPushArg:=0;
CTC.ExitThreadCallOp:=OPCODE_CALL;
CTC.ExitThreadCallArg:=DWORD(pCode)+ DWORD(@CTC.ExitThreadAddress)- DWORD(@CTC);
CTC.CTAddress:=pRemThread;
CTC.ExitThreadAddress:=DWORD(GetProcAddress( GetModuleHandle('kernel32.dll'), 'ExitThread'));

WriteProcessMemory(hProcess, pCode, @CTC, sizeof(CTC), bw);
hThread:=CreateRemoteThread(hProcess, nil, 0, pCode, nil, 0, bw);
WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProcess, pCode, sizeof(ctc), MEM_RELEASE);
CloseHandle(hProcess);
end; 

Разбираем этот код. Через ToolHelpAPI находим уникальный идентификатор процесса, в который будем внедрять поток. В моём примере это ‘Project2.exe’. Открываем процесс с параметрами на создание потока, операциями с виртуальной памятью и правом на запись в память.
Выделяем память под наш поток. Пару слов надо сказать о размере выделяемой памяти. Как вы можете заметить, я определяю размер процедуры с помощью вычисления разницы адресов 2-ух процедур. Одна из них это процедура, размер корой мне необходим, вторая это пустышка, находящаяся после нужного мне кода. Смотрите пример:

procedure GetMySize;
var
a: integer
begin
a:=10;
end;
procedure GetMySizeEnd; begin end;

Далее изменяю размер буфера, выделенного под код потока, на размер этого потока. Вношу в буфер этот код, используя функцию CopyMemory. И в
конце концов записываю его в выделенный участок памяти. Теперь у меня в памяти целевого процесса содержится код моего потока, и у меня в pRemThread хранится адрес его кода.

Теперь мы можем заполнять структуру вызова потока. Для этого выделяем в удалённом процессе место под эту запись (record). Заполняем её, записываем в выделенное под неё место. Создаём удалённый поток функцией CreateRemoteThread. Давайте посмотрим как мы заполняем эту структуру:

CTC.CTCO:= $15FF;
CTC.CTCA:=DWORD(pCode)+DWORD( @CTC.CTAddress)-DWORD(@CTC);
CTC.ExitThreadPushOp:= $68;
CTC.ExitThreadPushArg:=0;
CTC.ExitThreadCallOp:= $15FF;
CTC.ExitThreadCallArg:=DWORD(pCode)+ DWORD(@CTC.ExitThreadAddress)- DWORD(@CTC);
CTC.CTAddress:=pRemThread;
CTC.ExitThreadAddress:=DWORD(GetProcAddress( GetModuleHandle('kernel32.dll'), 'ExitThread'));

CTCO – опкод CALL
CTCA – аргумент CALL (адрес на адрес на наш внедрённый поток =))
ExitThreadPushOp – опкод PUSH
ExitThreadPushArg – аргумент PUSH
ExitThreadCallOp – опкод CALL
ExitThreadCallArg – аргумент CALL (адрес на адрес на ExitThread) 
CTAddress – адрес нашего потока
ExitThreadAddress – адрес функции ExitThread
Вот так это будет выглядеть на асме:

Call addr CTAddress
Push 0
Call addr ExitThreadAddress
CTAddress dd 12345678
ExitThreadAddress dd 87654321

Теперь начинается самое интересное =). Вся фишка в том, что в потоке мы не можем использовать НИКАКИЕ API-функции.
Надо твёрдо зашивать их в процедуру, но тогда в следующих версиях винды способ не будет работать. Решение состоит в том, чтобы передавать адреса через стек.
Приступаем к написанию потока, устанавливающего перехватчик. Немного модифицируем наш код, заполняющий структуру. Теперь она выглядит так:

TCTC2=packed record
CTPO4: byte;
CTPA4: dword;
CTPO3: byte;
CTPA3: dword;
CTPO2: byte;
CTPA2: dword;
CTPO1: byte;
CTPA1: dword;
CTCO: WORD;
CTCA: DWORD;
ExitThreadPushOp: byte;
ExitThreadPushArg: dword;
ExitThreadCallOp: word;
ExitThreadCallArg: dword;
ExitThreadAddress: dword;
CTAddress: DWORD;
end;

Добавилось 4 оператора PUSH. Они передают через стек адреса LoadLibraryA, GetProcAddress, FreeLibrary и адрес нашей процедуры-перехватчика. Заполняем дополнительные поля: 

CTC.CTPO4:=OPCODE_PUSH;
CTC.CTPA4:=pNewProc;
CTC.CTPO3:=OPCODE_PUSH;
CTC.CTPA3:=DWORD(GetProcAddress(GetModuleHandleA(kernel32), 'FreeLibrary'));
CTC.CTPO2:=OPCODE_PUSH;
CTC.CTPA2:=DWORD(GetProcAddress(GetModuleHandleA(kernel32), 'GetProcAddress'));
CTC.CTPO1:=OPCODE_PUSH;
CTC.CTPA1:=DWORD(GetProcAddress(GetModuleHandleA(kernel32), 'LoadLibraryA'));

pNewProc заполним позже. Теперь всё готово для написания внедряемого потока:

procedure RemThread;
type
TLoadLibraryA=function (lpLibFileName: PChar): HMODULE; stdcall;
TGetProcAddress=function (hModule: HMODULE; lpProcName: LPCSTR): FARPROC; stdcall;
TFreeLibrary=function (hLibModule: HMODULE): BOOL; stdcall;

TGetCurrentProcess=function : THandle; stdcall;
TWriteProcessMemory=function (hProcess: THandle; const lpBaseAddress: Pointer; lpBuffer: Pointer;
nSize: DWORD; var lpNumberOfBytesWritten: DWORD): BOOL; stdcall;
TReadProcessMemory=function (hProcess: THandle; const lpBaseAddress: Pointer; lpBuffer: Pointer;
nSize: DWORD; var lpNumberOfBytesRead: DWORD): BOOL; stdcall;

jmp_far=packed record
instr_push: byte;
push_operand: dword;
instr_ret: byte;
end;
var
LoadLibraryAAddr: Pointer;
GetProcAddressAddr: Pointer;
FreeLibraryAddr: Pointer;

_LoadLibraryA: TLoadLibraryA;
_GetProcAddress: TGetProcAddress;
_FreeLibrary: TFreeLibrary;

_GetCurrentProcess: TGetCurrentProcess;
_WriteProcessMemory: TWriteProcessMemory;
_ReadProcessMemroy: TReadProcessMemory;

hKernelLib: THandle;

GetCurrentProcessName: array [0..17] of char;
WriteProcessMemoryName: array [0..18] of char;
ReadProcessMemoryName: array [0..17] of char;

Kernel32LibName: array [0..12] of char;

InterceptedProcName: array [0..12] of char;

pTemp: Pointer;
instr: WORD;
pto: DWORD;
buf: jmp_far;
bw: Cardinal;

pNewProc: Pointer;
begin
Kernel32LibName[0]:='k';
Kernel32LibName[1]:='e';
Kernel32LibName[2]:='r';
Kernel32LibName[3]:='n';
Kernel32LibName[4]:='e';
Kernel32LibName[5]:='l';
Kernel32LibName[6]:='3';
Kernel32LibName[7]:='2';
Kernel32LibName[8]:='.';
Kernel32LibName[9]:='d';
Kernel32LibName[10]:='l';
Kernel32LibName[11]:='l';
Kernel32LibName[12]:=#0;

GetCurrentProcessName[0]:='G';
GetCurrentProcessName[1]:='e';
GetCurrentProcessName[2]:='t';
GetCurrentProcessName[3]:='C';
GetCurrentProcessName[4]:='u';
GetCurrentProcessName[5]:='r';
GetCurrentProcessName[6]:='r';
GetCurrentProcessName[7]:='e';
GetCurrentProcessName[8]:='n';
GetCurrentProcessName[9]:='t';
GetCurrentProcessName[10]:='P';
GetCurrentProcessName[11]:='r';
GetCurrentProcessName[12]:='o';
GetCurrentProcessName[13]:='c';
GetCurrentProcessName[14]:='e';
GetCurrentProcessName[15]:='s';
GetCurrentProcessName[16]:='s';
GetCurrentProcessName[17]:=#0;

WriteProcessMemoryName[0]:='W';
WriteProcessMemoryName[1]:='r';
WriteProcessMemoryName[2]:='i';
WriteProcessMemoryName[3]:='t';
WriteProcessMemoryName[4]:='e';
WriteProcessMemoryName[5]:='P';
WriteProcessMemoryName[6]:='r';
WriteProcessMemoryName[7]:='o';
WriteProcessMemoryName[8]:='c';
WriteProcessMemoryName[9]:='e';
WriteProcessMemoryName[10]:='s';
WriteProcessMemoryName[11]:='s';
WriteProcessMemoryName[12]:='M';
WriteProcessMemoryName[13]:='e';
WriteProcessMemoryName[14]:='m';
WriteProcessMemoryName[15]:='o';
WriteProcessMemoryName[16]:='r';
WriteProcessMemoryName[17]:='y';
WriteProcessMemoryName[18]:=#0;

ReadProcessMemoryName[0]:='R';
ReadProcessMemoryName[1]:='e';
ReadProcessMemoryName[2]:='a';
ReadProcessMemoryName[3]:='d';
ReadProcessMemoryName[4]:='P';
ReadProcessMemoryName[5]:='r';
ReadProcessMemoryName[6]:='o';
ReadProcessMemoryName[7]:='c';
ReadProcessMemoryName[8]:='e';
ReadProcessMemoryName[9]:='s';
ReadProcessMemoryName[10]:='s';
ReadProcessMemoryName[11]:='M';
ReadProcessMemoryName[12]:='e';
ReadProcessMemoryName[13]:='m';
ReadProcessMemoryName[14]:='o';
ReadProcessMemoryName[15]:='r';
ReadProcessMemoryName[16]:='y';
ReadProcessMemoryName[17]:=#0;

InterceptedProcName[0]:='R';
InterceptedProcName[1]:='e';
InterceptedProcName[2]:='s';
InterceptedProcName[3]:='u';
InterceptedProcName[4]:='m';
InterceptedProcName[5]:='e';
InterceptedProcName[6]:='T';
InterceptedProcName[7]:='h';
InterceptedProcName[8]:='r';
InterceptedProcName[9]:='e';
InterceptedProcName[10]:='a';
InterceptedProcName[11]:='d';
InterceptedProcName[12]:=#0;

asm
mov eax, [ebp+8]
mov LoadLibraryAAddr, eax
mov eax, [ebp+12]
mov GetProcAddressAddr, eax
mov eax, [ebp+16]
mov FreeLibraryAddr, eax
mov eax, [ebp+20]
mov pNewProc, eax
end;

@_LoadLibraryA:=LoadLibraryAAddr;
@_GetProcAddress:=GetProcAddressAddr;
@_FreeLibrary:=FreeLibraryAddr;

hKernelLib:=_LoadLibraryA(@Kernel32LibName);

@_GetCurrentProcess:=_GetProcAddress( hKernelLib, @GetCurrentProcessName);
@_ReadProcessMemroy:=_GetProcAddress( hKernelLib, @ReadProcessMemoryName);
@_WriteProcessMemory:=_GetProcAddress( hKernelLib, @WriteProcessMemoryName);

pTemp:=_GetProcAddress(hKernelLib, @InterceptedProcName);
_ReadProcessMemroy(_GetCurrentProcess, pTemp, @instr, 2, bw);
_ReadProcessMemroy(_GetCurrentProcess, Pointer(DWORD(pTemp)+2), @pto, 4, bw);
if instr=$25FF then
pTemp:=Pointer(pto);
buf.instr_push:=$68;
buf.push_operand:=DWORD(pNewProc);
buf.instr_ret:=$C3;
_WriteProcessMemory(_GetCurrentProcess, pTemp, @buf, 6, bw);

_FreeLibrary(hKernelLib);
end;
procedure RemThreadEnd; begin end;

В этом коде нет ничего сложного, всё это я разобрал выше. Есть одно замечание – нельзя использовать тип string. Параметры получаем через стек: [ebp+8] – первый параметр, [ebp+адрес предыдущего параметра + размер предыдущего параметра] – схема поучения параметров.

Перед нами встаёт ещё одна проблема: процедура-обработчик имеет такое же количество параметров, как и перехватываемая функция, и данные передаёт чужой процесс, следовательно мы не можем передавать адреса процедур. Поэтому я решил использовать модификацию кода на лету. В основную процедуру добавляем переменные:

hwa, lwa: WORD;
New: array [0..4] of byte;
i: integer;
pNewProc: Pointer;
SCPA: Cardinal;

Расскажу немного об модификации кода. В коде потока-перехватчика делаем ассемблерную вставку: 

asm
push $12345678
pop eax
end;

Помещаем процедуру в буфер. Находим в ней сигнатуру push $12345678. В коде это выглядит как массив байтов = ($68, $78, $56, $34, $12). Байты адреса записываются задом наперёд. $68 – опкод команды PUSH. Следовательно адрес функции мы должны записать также задом наперёд.Все эти действия выполняет этот код:

SetLength(fCode, DWORD(@NewProcEnd)-DWORD(@NewProc));
CopyMemory(fCode, @NewProc, length(fCode));

SCPA:=DWORD(GetProcAddress( GetModuleHandleA(user32), 'SetCursorPos'));

New[0]:=$68;
hwa:=HIWORD(SCPA);
lwa:=LOWORD(SCPA);
New[1]:=LOBYTE(lwa);
New[2]:=HIBYTE(lwa);
New[3]:=LOBYTE(hwa);
New[4]:=HIBYTE(hwa);

for i:=0 to length(fCode)-5 do
if (fCode[i]=SIGNATURE_TEST[0]) and (fCode[i+1]=SIGNATURE_TEST[1])
and (fCode[i+2]=SIGNATURE_TEST[2])
and (fCode[i+3]=SIGNATURE_TEST[3]) and (fCode[i+4]=SIGNATURE_TEST[4]) then
begin
fCode[i]:=New[0];
fCode[i+1]:=New[1];
fCode[i+2]:=New[2];
fCode[i+3]:=New[3];
fCode[i+4]:=New[4];
break;
end;

hProcess:=OpenProcess(PROCESS_CREATE_THREAD or PROCESS_VM_WRITE or PROCESS_VM_OPERATION,
false, GetPIDByName('Project2.exe'));
pNewProc:=DWORD(VirtualAllocEx(hProcess, nil, DWORD(@NewProcEnd)-DWORD(@NewProc),
MEM_COMMIT, PAGE_EXECUTE_READWRITE));
WriteProcessMemory(hProcess, Pointer(pNewProc), fCode, length(fCode), bw);
CloseHandle(hProcess);

Вводим константу SIGNATURE_TEST: array [0..4] of byte = ($68, $BA, $17, $9C, $5D); 

Этот код также очень простой, его вставляем между записью внедряемого потока и выделением памяти под структуру вызова потока.
После всего этого мы можем написать процедуру-перехватчик.

procedure NewProc;
type
TSetCursorPos=function (X, Y: Integer): BOOL; stdcall;
var
SetCursorPosAddr: Pointer;
_SetCursorPos: TSetCursorPos;

Param: DWORD;
begin
asm
mov eax, [ebp+8]
mov Param, eax
push $5D9C17BA
pop eax
mov SetCursorPosAddr, eax
end;
@_SetCursorPos:=SetCursorPosAddr;
_SetCursorPos(100, 100);
asm
mov esp, ebp
pop ebp
ret 4
end;
end;
procedure NewProcEnd; begin end;

Эта процедура вызывается вместо ResumeThread, это можно изменить во внедряемом потоке. Она изменяет положение курсора. Ассемблерная вставка после всего кода
совершает выход из подпрограммы. Ret 4 – очистка стека после параметров, число 4 – размер ВСЕХ параметров.

Вот и конец. Теперь можно без использования дополнительных библиотек перехватывать API функции.

Исходники

Check Also

Изучаем ПЛК. Краткий гайд по поиску уязвимостей в промышленных контроллерах

Если ты думаешь, что контроллеры, которые ставят в зданиях и на заводах, защищены намного …

Оставить мнение