Внимание. Нижеизложенный метод пригоден только для *NT. В 9x он, к сожалению, не работоспособен!
Следующая вариация предыдущего способа состоит в правке оригинальной API функции. Её мы и рассмотрим
Рис. Как получить реальный адрес процедуры
Как это происходит
Мы будем делать глобальный перехватчик. Очень удобным представляется подключение нашей DLL, которая будет производить перехват. Остается лишь подключить эту самую DLL. Для этого, можно прибегнуть к функции SetWindowsHookEX, которая подключает DLL к большинству (но не ко всем!!!) запущеных процессов. Однако мы будем действовать более основательно. На первом этапе - мы с помощью функции CreateRemoteThread внедрим свой код в чужое приложение. Этод код затем загрузит нашу DLL. Потом - внесем изменения в такие API как CreateProcess (для NT, 2k, 2k sp1, 2k sp2) и CreateProcessInternal (в двух вариациях - для XP и для 2k sp3). При создании процесса - будет вызываться наш обработчик который создаст замороженный процесс, перенаправит его главный thread на нужный код (который загрузит DLL и вернет все на свои места) и "разморозит" процесс
Момент 1 - Как мы будем перехватывать API функцию?
В кратце (на примере WinExec):
Мы узнаём реальный адрес (см. рис.) по которому размещена процедура (не просто @WinExec; по @WinExec размещен переход на другой (реальный) адрес!). Далее - мы переносим заголовок (6 байт) этой процедуры в спец-буфер. Вместо перемещенных 6 байт, мы вставляем ссылку на свою процедуру. В нашей процедуре (если мы решим позволить вызов перехватываемой функции) мы: 1) вернем функцию в первозданный вид (восстановим 6 байт из спец-буфера), 2) вызовем функцию, 3) опять изменим заголовок функции. Момент 2 - Как мы будем вставлять свой код?
В случае если мы знаем только PID - мы используем CreateRemoteThread: 1) выделяем память в чужом процессе, 2) заполняем структуру актуальными данными (адреса процедур, название DLL), 3) записываем ее в выделенную нами область памяти 4) вызываем CreateRemoteThread
Когда нам известен хэндл thread'a - используем Get/SetThreadContext для изменения текущей позиции исполнения (регистр EIP): 1) выделяем память в чужом процессе, 2) записываем в нее прототип кода, 3) записываем туда свои данные (название DLL) 4) правим все ссылки в прототипе кода, записанном в чужую память, 5) ставим значение регистра EIP равное адресу, по которому мы записали прототип
Что нужно знать
Инструкция перехода по адресу, указанному Pointer'ом (JMP ptr) в памяти выгладит так: FF 25 xx xx xx xx, где xx xx xx xx - адрес указателя на процедуру
Инструкция относительного перехода от текущего адреса в памяти выгладит так: E9 xx xx xx xx xx, где xx xx xx xx - кол-во байт, на которое нужно сдвинуться (вверх или вниз - в зависимости от знака)
push $AAAAAAAA / ret - переход на адрес $AAAAAAAA
Также желательно умение работы с окном "CPU" (Ctrl+Alt+C во время отладки).
Чего следует избегать, действуя в контексте других приложений
Разумеется вы должны быть бдительны, однако, как показала практика, и этого недостаточно. Разберем простой пример:
TImportCode = packed record
JumpInstruction: Word; // should be $25FF
AddressOfPointerToFunction: PPointer; end;
PImportCode = ^TImportCode;
function FinalFunctionAddress(Code: Pointer): Pointer; var
rd: word;
c: cardinal; begin
result:=code; if result=nil then exit;
readprocessmemory(getcurrentprocess, code, @rd, 2, c); if rd=$25ff then begin
result:=pimportcode(code).AddressOfPointerToFunction^; end; end;
Где ошибка? Правильно. Ошибка в строке "result:=pimportcode(code).AddressOfPointerToFunction^;". Действительно: а что если pimportcode(code).AddressOfPointerToFunction указывает на недоступную область памяти? Тогда произойдет сбой или крах вызывающего приложения.
O.K. Теперь допустим, что нам доступны полностью 4 GB памяти. Где тогда ошибка? Ошибки нет? Нет - ошибка есть. Как ни странно, но данный код может давать сбой! Вместо этого необходимо делать так:
function FinalFunctionAddress(Code: Pointer): Pointer; var
rd: word;
c: cardinal; begin
result:=code; if result=nil then exit;
readprocessmemory(getcurrentprocess, code, @rd, 2, c); if rd=$25ff then begin
readprocessmemory(getcurrentprocess, pointer(dword(code)+2), @result, 4, c);
readprocessmemory(getcurrentprocess, result, @result, 4, c); end; end;
Несмотря на то, что с логической точки зрения оба кода идентичны (для указанного условия доступности всей ОЗУ), второй будет работать корректно всегда, тогда как первый (работая в контексте другого приложения), в зависимости от фазы луны может давать верный или неверный результат.
Более того, как показала практика, даже вызов такой функции, как GetProcAddress(GetModuleHandle('kernel32.dll'), 'LoadLibraryA') (что равносильно использованию @LoadLibraryA) может возвращать то - корректный, то - некорректный результат, если данная API вызывается из контекста другого приложения в Win 2k SP3.
Пойдем дальше. Пусть у нас есть код:
pTarget := VirtualAllocEx( hProcess, nil, SizeOf(InjectInfo), MEM_COMMIT, PAGE_EXECUTE_READWRITE); //выделяем память в чужом процессе
На первый взгляд все в порядке. Однако, эта процедура может стать причиной "интересных" проблем. Так, например, в Win2k sp3/msc.exe (dfrg.msc) при таком вмешательстве начинает циклично обращаться к диску A:, сопровождая процесс характерными звуками и загоранием зеленой лампочки. Решение проблемы - пока не найдено.
Теперь приведем листинг нашей DLL unit ApiHijackNT;
interface uses
Windows, PSApi, PEStuff;
type
TRemoteData = packed record //своеобразная структура для удобства внедрения стороннего кода
LoadLib, FreeLib, GetModH: DWORD; //место для хранения адресов функций
LibPath: string [255];
Code : array [0..127] of Byte; end;
PRemoteData = ^TRemoteData;
function RemoteFunction1(data: PRemoteData): DWORD; stdcall; //функция, которая будет загружать DLL при использовании CreateRemoteThread type
TLL = function (lpLibFileName: PChar): HMODULE; stdcall; var
LL: TLL; begin
@LL:=Pointer(Data^.LoadLib);
LL(@(Data^.LibPath[1])); end;
procedure RemoteProc; assembler; //прототип удаленной функции для загрузки DLL с помощью правки контекста
Внимание! По хорошему - нам надо сохранить не только стандартные регистры, но и спец регистры (флаги; где например хранится результат сравнения CMP) (инструкции pusha/popa, pushf/popf). Если же мы внедримся между CMP и J*/JN*, то, скорее всего, нарушим правильность выполнения программы! Однако в нашем случае - мы внедряемся еще до того, как программа начнет свою работу, поэтому все должно быть ОК. Если данный способ все же будет не применим, то необходимо делать сохранение контекста, передачу его удаленной процедуре, которая создаст поток. Этот поток приостановит поток-создатель, восстановит его контекст, и возобновит его работу. Дабы не загружать читателя, здесь этот вариант не приводится.
asm
push EAX
push EBX
push ECX
push EDX
push ESI
push EDI
push EBP
push ESP
push $FEFEFEFE //PChar - название DLL
mov EAX,$CECECECE //адрес LoadLibrary
call EAX
pop ESP
pop EBP
pop EDI
pop ESI
pop EDX
pop ECX
pop EBX
pop EAX
push $ABABABAB //адрес, на котором мы заморозили thread
ret end;
procedure RemoteFunction2(data: PRemoteData); stdcall; //Функция, которая будет выгружать DLL type
TGMH = function (lpModuleName: PChar): HMODULE; stdcall;
TFL = function (hLibModule: HMODULE): BOOL; stdcall; var
GMH: TGMH;
FL: TFL;
d: DWORD; begin
@GMH:=Pointer(Data^.GetModH);
@FL:=Pointer(Data^.FreeLib);
d:=GMH(@(Data^.LibPath[1]));
FL(d); end;
procedure DoRemoteJob(PID: DWORD; Load: boolean; hProc: DWORD);stdcall; //функция, внедряющая сторонний код
//если заданы (т.е. >0) одновременно и PID и hProc, то PID выступает в роли хэндла удаленного Thread'a var
hProcess : THandle;
InjectInfo : TRemoteData;
pTarget, pTarget2, pTarget0: PRemoteData;
dwThreadId, BytesWritten:DWORD ;
hThread : THandle;
ty: pchar;
by, p: Pointer;
Context: _Context;
OEIP: DWORD; begin if PID=GetCurrentProcessID then exit;
GetMem(by, 1024);
BytesWritten:=GetModuleFileName(hInstance, by, 1024); //мы будем подключать сами себя
InjectInfo.LibPath:=Copy(pchar(by), 1, byteswritten);
InjectInfo.LibPath[BytesWritten+1]:=#0;
FreeMem(by);
InjectInfo.LoadLib:=DWORD(FinalFunctionAddress(@LoadLibraryA));
InjectInfo.FreeLib:=DWORD(FinalFunctionAddress(@FreeLibrary));
InjectInfo.GetModH:=DWORD(FinalFunctionAddress(@GetModuleHandle));
if hProc=0 then
hProcess := OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid) //открываем процесс, если это еще не сделано else hProcess:=hProc;
try if (hproc=0)or(PID=0) then begin
pTarget := VirtualAllocEx( hProcess, nil, SizeOf(InjectInfo), MEM_COMMIT, PAGE_EXECUTE_READWRITE); //выделяем память в чужом процессе if load then WriteProcessMemory(GetCurrentProcess, @(InjectInfo.Code[0]), @RemoteFunction1, SizeOf(InjectInfo.Code), BytesWritten) else WriteProcessMemory(GetCurrentProcess, @(InjectInfo.Code[0]), @RemoteFunction2, SizeOf(InjectInfo.Code), BytesWritten); //записываем в структуру свои процедуры ...
WriteProcessMemory(hProcess, pTarget, @InjectInfo, SizeOf(InjectInfo), BytesWritten); //записываем структуру в чужое приложение
WaitForSingleObject(hThread, INFINITE); //ждем завершения выполнения нашего кода. ВНИМАНИЕ: иногда здесь может быть зависание. По хорошему - надо установить таймаут.
CloseHandle(hThread); end; if (hproc<>0)and(PID<>0) then begin
Context.ContextFlags := CONTEXT_FULL;
SuspendThread(PID);
GetThreadContext(PID, Context); //получаем контекст чужого thread'a
pTarget := VirtualAllocEx( hProcess, nil, 512, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if dword(pTarget)<>0 then //не срабатывает в 16-битных приложениях begin
WriteProcessMemory(hProcess, pTarget, @remoteproc, 64, BytesWritten); //записываем нашу процедуру в чужой процесс
OEIP:=dword(pointer(pTarget))+64;
WriteProcessMemory(hProcess, pointer(dword(pTarget)+9), @OEIP, 4, BytesWritten); //записываем адрес названия DLL
WriteProcessMemory(hProcess, pointer(dword(pTarget)+14), @InjectInfo.LoadLib, 4, BytesWritten); //@LoadLib
WriteProcessMemory(hProcess, pointer(dword(pTarget)+29), @Context.Eip, 4, BytesWritten); //куда мы должны вернуться после загрузки DLL
WriteProcessMemory(hProcess, pointer(dword(pTarget)+64), @InjectInfo.LibPath[1], length(InjectInfo.LibPath), BytesWritten); // записываем название DLL
Context.Eip:=DWORD(pTarget); //перенаправляем чужой thread на наш код
SetThreadContext(PID, context); endelse {censored};
ResumeThread(PID); end;
finally if ( hProcess > 0) thenif hproc=0 then begin if Assigned( pTarget ) then
VirtualFreeEx( hProcess, pTarget, SizeOf(InjectInfo), MEM_RELEASE);
CloseHandle(hProcess); end; end; end;
function NewCreateProcessW(lpApplicationName: PWideChar; lpCommandLine: PWideChar;
lpProcessAttributes, lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer;
lpCurrentDirectory: PWideChar; const lpStartupInfo: TStartupInfo; var lpProcessInformation: TProcessInformation): BOOL; stdcall; var
resume: boolean; begin if {условие} then begin
UnPatch(CreateProcessWBuf, @NewCreateProcessW, FinalFunctionAddress(@CreateProcessW), false); //приводим функцию в первозданный вид
resume:=(dwCreationFlags and CREATE_SUSPENDED) =0; //форсируем флаг CREATE_SUSPENDED
dwCreationFlags:=dwCreationFlags and CREATE_SUSPENDED; asm
push lpProcessInformation
push lpStartupInfo
push lpCurrentDirectory
push lpEnvironment
push dwCreationFlags
push bInheritHandles
push lpThreadAttributes
push lpProcessAttributes
push lpCommandLine
push lpApplicationName
call CreateProcessW
mov result, eax end;
DoRemoteJob(lpProcessInformation.hThread, 1=1, lpProcessInformation.hProcess); //внедряем себя в созданный процесс if resume then ResumeThread(lpProcessInformation.hThread);
Patch(CreateProcessWBuf, @NewCreateProcessW, FinalFunctionAddress(@CreateProcessW), false); //ставим перехватчик обратно endelse result:=false; end;
{routine for WinXP build 5.2600} function NewCreateProcessInternalW1(stack_shifter1: pointer; lpApplicationName: PWideChar; lpCommandLine: PWideChar;
lpProcessAttributes, lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer;
lpCurrentDirectory: PWideChar; const lpStartupInfo: TStartupInfo; var lpProcessInformation: TProcessInformation; stack_shifter2: pointer): BOOL; stdcall; var
resume: boolean; begin ifthen begin
UnPatch(CreateProcessInternalWBuf, @NewCreateProcessInternalW1, FinalFunctionAddress(atcpiw), false);
resume:=(dwCreationFlags and CREATE_SUSPENDED) =0;
dwCreationFlags:=dwCreationFlags and CREATE_SUSPENDED; asm
push stack_shifter2
push lpProcessInformation
push lpStartupInfo
push lpCurrentDirectory
push lpEnvironment
push dwCreationFlags
push bInheritHandles
push lpThreadAttributes
push lpProcessAttributes
push lpCommandLine
push lpApplicationName
push stack_shifter1
call atcpiw
mov result, eax end;
DoRemoteJob(lpProcessInformation.hThread, 1=1, lpProcessInformation.hProcess); if resume then ResumeThread(lpProcessInformation.hThread);
Patch(CreateProcessInternalWBuf, @NewCreateProcessInternalW1, FinalFunctionAddress(atcpiw), false); endelse result:=false; end;
{routine for Win2k SP3} function NewCreateProcessInternalW2(stack_shifter1: pointer; lpApplicationName: PWideChar; lpCommandLine: PWideChar;
lpProcessAttributes, lpThreadAttributes: PSecurityAttributes;
bInheritHandles: BOOL; dwCreationFlags: DWORD; lpEnvironment: Pointer;
lpCurrentDirectory: PWideChar; const lpStartupInfo: TStartupInfo; var lpProcessInformation: TProcessInformation): BOOL; stdcall; var
resume: boolean; begin ifthen begin
UnPatch(CreateProcessInternalWBuf, @NewCreateProcessInternalW2, FinalFunctionAddress(atcpiw), false);
resume:=(dwCreationFlags and CREATE_SUSPENDED) =0;
dwCreationFlags:=dwCreationFlags and CREATE_SUSPENDED; asm
push lpProcessInformation
push lpStartupInfo
push lpCurrentDirectory
push lpEnvironment
push dwCreationFlags
push bInheritHandles
push lpThreadAttributes
push lpProcessAttributes
push lpCommandLine
push lpApplicationName
push stack_shifter1
call atcpiw
mov result, eax end;
DoRemoteJob(lpProcessInformation.hThread, 1=1, lpProcessInformation.hProcess); if resume then ResumeThread(lpProcessInformation.hThread);
Patch(CreateProcessInternalWBuf, @NewCreateProcessInternalW2, FinalFunctionAddress(atcpiw), false); endelse result:=false; end;
procedure InitPlatformId; var
OSVersionInfo: TOSVersionInfo; begin
OSVersionInfo.dwOSVersionInfoSize := SizeOf(OSVersionInfo); if GetVersionEx(OSVersionInfo) then with OSVersionInfo do begin
Win32Platform := dwPlatformId;
Win32MajorVersion := dwMajorVersion;
Win32MinorVersion := dwMinorVersion; end; end;
function FinalFunctionAddress(Code: Pointer): Pointer; //если нас перенаправляют куда-то, то смотрим куда var
rd: word;
c: cardinal; begin
result:=code; if result=nil then exit;
readprocessmemory(getcurrentprocess, code, @rd, 2, c); if rd=$25ff then begin
readprocessmemory(getcurrentprocess, pointer(dword(code)+2), @result, 4, c);
readprocessmemory(getcurrentprocess, result, @result, 4, c); end; end;
procedure Patch(var Buf: PChar; NewF, OldF: pointer; MakeBuf: boolean = true); //ставим перхватчик var
q, jto: DWORD;
instr: byte; begin if MakeBuf then GetMem(Buf, 6); //нам надо выделять память или нет?
ReadProcessMemory(GetCurrentProcess, OldF, Buf, 6, q); //сохраняем оригинальные 6 байт
procedure HookCProc; begin
atcpiw:=GetProcAddress(GetModuleHandle('kernel32.dll'), 'CreateProcessInternalW'); //XP или Win2K SP3? if atcpiw<>nil then XP:=true else XP:=false; ifnot XP then begin
Patch(CreateProcessWBuf, @NewCreateProcessW, FinalFunctionAddress(@CreateProcessW)); endelse begin if (Win32MajorVersion=5) and (Win32MinorVersion=0) then
Patch(CreateProcessInternalWBuf, @NewCreateProcessInternalW2, FinalFunctionAddress(atcpiw)) else
Patch(CreateProcessInternalWBuf, @NewCreateProcessInternalW1, FinalFunctionAddress(atcpiw)); end; end;
procedure UnhookCProc; begin ifnot XP then begin
UnPatch(CreateProcessWBuf, @NewCreateProcessW, FinalFunctionAddress(@CreateProcessW), true); endelse begin if (Win32MajorVersion=5) and (Win32MinorVersion=0) then
UnPatch(CreateProcessInternalWBuf, @NewCreateProcessInternalW2, FinalFunctionAddress(atcpiw)) else
UnPatch(CreateProcessInternalWBuf, @NewCreateProcessInternalW1, FinalFunctionAddress(atcpiw)); end; end;
Замечание
При запуске, эту DLL (а исполнять этот код надо только из DLL) надо внедрить во все процессы, активность которых мы хотим отслеживать. Делается это вызовом DoRemoteJob(PID, true, 0); из библиотеки (где PID - PID процесса в который мы хотим себя загрузить). Для выгрузки соответсвенно надо будет вызвать DoRemoteJob(PID, false, 0);
Copyright
Перепечатка статьи разрешается только с уведомления автора и ссылкой на первоисточник.