Внимание. Нижеизложенный метод пригоден только для *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;
 
 
 procedure HookCProc;
 procedure UnhookCProc;
 procedure Patch(var Buf: PChar; NewF, OldF: pointer; MakeBuf: boolean = true);
 procedure UnPatch(var Buf: PChar; NewF, OldF: pointer; DelBuf: boolean = true);
 function FinalFunctionAddress(Code: Pointer): Pointer;
 procedure DoRemoteJob(PID: DWORD; Load: boolean; hProc: DWORD); stdcall;
 
 implementation
 
 var
     atcpiw: pointer;
     CreateProcessWBuf,
     CreateProcessInternalWBuf: PChar;
 
   q: DWORD;
   xp, finit: boolean;
   Win32Platform: integer = 0;
   Win32MajorVersion: integer = 0;
   Win32MinorVersion: integer = 0;
 
 
 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); //записываем структуру в чужое приложение
 
      hThread := CreateRemoteThread( hProcess, nil, 0, @(pTarget^.Code[0]), pTarget, 0, dwThreadId); //создаем свой thread
 
      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);
         end else {censored};
       ResumeThread(PID);
     end;
  finally
    if ( hProcess > 0) then if 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); //ставим перехватчик обратно
   end else 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
   if then
   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);
   end else 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
   if then
   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);
   end else 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 байт
 
       instr:=$E9; // JMP
       WriteProcessMemory(GetCurrentProcess, OldF, @instr, 1, q); // записываем редирект
       jto:=-Dword(OldF)+dword(NewF)-5;
       WriteProcessMemory(GetCurrentProcess, pointer(Dword(OldF)+1), @jto, 4, q);
 end;
 
 procedure UnPatch(var Buf: PChar; NewF, OldF: pointer; DelBuf: boolean = true); //снимаем перхватчик
 begin
       WriteProcessMemory(GetCurrentProcess, OldF, Buf, 6, q);
 
       if delbuf then FreeMem(Buf);
 end;
 
 
 procedure HookCProc;
 begin
   atcpiw:=GetProcAddress(GetModuleHandle('kernel32.dll'), 'CreateProcessInternalW'); //XP или Win2K SP3?
   if atcpiw<>nil then XP:=true else XP:=false;
   if not XP then
     begin
       Patch(CreateProcessWBuf, @NewCreateProcessW, FinalFunctionAddress(@CreateProcessW));
     end else
     begin
       if (Win32MajorVersion=5) and (Win32MinorVersion=0) then
         Patch(CreateProcessInternalWBuf, @NewCreateProcessInternalW2, FinalFunctionAddress(atcpiw))
       else
         Patch(CreateProcessInternalWBuf, @NewCreateProcessInternalW1, FinalFunctionAddress(atcpiw));
     end;
 end;
 
 procedure UnhookCProc;
 begin
   if not XP then
     begin
       UnPatch(CreateProcessWBuf, @NewCreateProcessW, FinalFunctionAddress(@CreateProcessW), true);
     end else
     begin
       if (Win32MajorVersion=5) and (Win32MinorVersion=0) then
         UnPatch(CreateProcessInternalWBuf, @NewCreateProcessInternalW2, FinalFunctionAddress(atcpiw))
       else
         UnPatch(CreateProcessInternalWBuf, @NewCreateProcessInternalW1, FinalFunctionAddress(atcpiw));
     end;
 end;
 
 initialization
   InitPlatformId;
   HookCProc;
 finalization
   UnhookCProc;
 end.


Замечание
При запуске, эту DLL (а исполнять этот код надо только из DLL) надо внедрить во все процессы, активность которых мы хотим отслеживать. Делается это вызовом DoRemoteJob(PID, true, 0); из библиотеки (где PID - PID процесса в который мы хотим себя загрузить). Для выгрузки соответсвенно надо будет вызвать DoRemoteJob(PID, false, 0);

Copyright
Перепечатка статьи разрешается только с уведомления автора и ссылкой на первоисточник.

Top100 Rambler's Top100
Hosted by uCoz