Remplacer une fonction d'une dll par n'importe quelle autre!

Description

Ce code vous permettra de redéfinir le code d'une fonction importée statiquement par une DLL dans votre programme par une fonction quelconque définie dans votre programme. Il y a très peu à faire pour modifier le code afin qu'il fasse la même chose dans un processus extérieur. La fonction ReplaceAPI de l'unité APIRedirect.pas fait tout le travail:
-Détermination de l'adresse de base où est chargé l'exécutable.
-Exploration de la table des symboles importés jusqu'à trouver la fonction initiale à remplacer
-Recopiage de code machine à partir d'un petit morceau d'assembleur "inline" encadré par 2 nombres magiques pour retrouver le code machine correspondant
-Ce morceau de code "Launcher" est ensuite modifié pour y mettre l'adresse de la nouvelle fonction désirée
-Ajout d'une section exécutable à l'exécutable contenant le code du launcher de la nouvelle fonction
-Et ça marche!

Attention toutefois à respecter SCRUPULEUSEMENT le nombre et le format des paramètres de la nouvelle fonction à remplacer. Et ne pas oublier les conventions d'appels (stdcall, etc...) sinon ça va tout faire planter. Par exemple, si vous jugez que la fonction MessageBox de Windows ne vous convient pas, vous pouvez utiliser:

function MyMessageBox(hWnd: HWND; lpText, lpCaption: PChar; uType: UINT): Integer; stdcall;
begin
<-Code personnel pour remplacer la fonction originale de Microsoft
end;

ReplaceAPI(kernel32,'MessageBoxA',@MyMessageBox);

Et le tour est joué!

Je fourni un petit programme d'exemple pour montrer l'utilité d'une telle fonction: il se charge de remplacer l'API GetSysColor de Microsoft par une autre fonction qui permet de changer les couleurs systèmes. Tous les composants VCL non natifs (c'est à dire qui ne sont pas entièrement peints par Windows) sont affectés. Et pas besoin de changer leur couleur!

J'admet que le résultat est visuellement assez pauvre, mais le principe est là.

Source / Exemple :


unit APIRedirect;

interface

uses
  SysUtils,Windows,ImageHlp,Dialogs;

function GetImageBase(
  Process:THandle;   // Handle to a valid process object. Current thread must have PROCESS_VM_READ | PROCESS_VM_OPERATION access to it.

  Thread:THandle     // Handle to a valid thread object. Current thread must have THREAD_GET_CONTEXT access to it.
  
):Pointer;           // Return the image base of the process.

function ReplaceAPI(
  DllName:string;    // Name of the DLL containing the procedure or function to replace
  
  ProcName:string;   // Name of the procedure or function to replace

  NewProc:Pointer;   // Pointer to the new desired procedure. The declaration of this procedure *MUST* be *EXACTLY* the same as the original API.

  Process:THandle=0; // Handle to a valid process object (or zero for the current process). The caller must have
                     // PROCESS_SUSPEND_RESUME | PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION
                     // access to it.

  Thread:THandle=0   // Handle to a valid thread object (or zero for the current thread). The caller must have
                     // THREAD_GET_CONTEXT | THREAD_SUSPEND_RESUME | THREAD_QUERY_INFORMATION
                     // access to it.
  
):Boolean;overload;  // Return True if the API was successfully patched, false otherwise (for instance, if the process doesnt use the specified
                     // name in his import directory, or if the API was not found in the specified DLL).
                     
type
  PPEB=^TPEB;
  TPEB=packed record
    UselessData:array[0..1] of Cardinal;   // This is of course not the real definition, but only the ImageBaseAddress field is of interest in our case
    ImageBaseAddress:POINTER;
  end;

  PTEB=^TTEB;
  TTEB=packed record
    UselessData:array[0..11] of Cardinal;  // This is of course not the real definition, but only the PEB field is of interest in our case
    PEB:PPEB;
  end;

  IMAGE_IMPORT_BY_NAME=packed record
    Hint:Word;
    Name:CHAR;
  end;
  TImageImportByName=IMAGE_IMPORT_BY_NAME;
  PImageImportByName=^IMAGE_IMPORT_BY_NAME;

  IMAGE_THUNK_DATA=packed record
    ForwarderString: PBYTE;
    Func: PDWORD;
    Ordinal: DWORD;
    AddressOfData:PImageImportByName;
  end;
  TImageThunkData=IMAGE_THUNK_DATA;
  PImageThunkData=^IMAGE_THUNK_DATA;

  TIIDMisc=packed record
    case Integer of
      0:(Characteristics:DWORD);    // 0 for terminating null import descriptor
      1:(OriginalFirstThunk:DWORD); // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
  end;

  IMAGE_IMPORT_DESCRIPTOR=packed record
    Misc:TIIDMisc;
    TimeDateStamp:DWORD;
    ForwarderChain:DWORD;
    Name:DWORD;
    FirstThunk:DWORD;
  end;
  TImageImportDescriptor=IMAGE_IMPORT_DESCRIPTOR;
  PImageImportDescriptor=^IMAGE_IMPORT_DESCRIPTOR;

const
  LOADER_START_MAGIC=$FAFAFAFA;   // Magic flags arbitrary defined. These flags will allow us to retrieve
  LOADER_END_MAGIC=$FEFEFEFE;     // code offsets in asm code. Anyway, let us hope the generated machine code doed not
  LOADER_PROC_MAGIC=$ABABABAB;    // contain them! (I havent checked, but the probability is highliy negligible)

implementation

procedure Launcher;  // A launcher for the new procedures. We insert flags to extract only the required code, without whatever Delphi will add
                     // for initialisation and finalisation of the call
asm
              DD   LOADER_START_MAGIC; // Flag to indicate start of code
              PUSH EBP                 // Store EBP to API caller
              CALL @START
@START:       POP  EBP
              SUB  EBP, OFFSET @START  // Get base EBP
              MOV  EAX, [EBP+@@PROC]   // Calculate new proc offset
              POP  EBP                 // Restore EBP to API caller
              JMP  EAX                 // JMP to new procedure address (no need to CALL)
@@PROC:       DD   LOADER_PROC_MAGIC;  // Flag to indicate absolute address of new proc
              DD   LOADER_END_MAGIC;   // Flag to indicate end of code
end;

function DataPos(Start:Pointer;Data:Cardinal):Cardinal;
begin
  Result:=Cardinal(Start);
  while PCardinal(Result)^<>Data do
    Inc(Result);
end;

procedure GetLauncherCode(var Code:Pointer;var Size:Cardinal;Proc:Pointer);
var
  p:Cardinal;
begin
  p:=DataPos(@Launcher,LOADER_START_MAGIC)+4;  // Retrieve position of launching code inside Launcher
  Size:=DataPos(@Launcher,LOADER_END_MAGIC)-p; // Retrieve end of launching code
  GetMem(Code,Size);
  CopyMemory(Code,Pointer(p),Size);            // Duplicate launching code
  p:=DataPos(Code,LOADER_PROC_MAGIC);          // Replace the absolute procedure address (which contains LOADER_PROC_MAGIC)...
  PPointer(p)^:=PPointer(@Proc)^;              // ...with the new one
end;

function GetImageBase(Process:THandle;Thread:THandle):Pointer;  // This function uses undocumented APIs. One can find their descriptions there:
var                                                             //            http://undocumented.ntinternals.net/
  Context:TContext;                                             // There are other (simplier) means to obtain the image base, but this one
  SelEntry:TLDTEntry;                                           // can extract the image base of other running processes.
  FSBase:Cardinal;
  TEB:TTEB;
  PEB:TPEB;
  n:Cardinal;
begin
  Context.ContextFlags:=CONTEXT_FULL or CONTEXT_DEBUG_REGISTERS;
  if not GetThreadContext(Thread,Context) then                  // See the above link for additional informations
    RaiseLastOSError;
  if not GetThreadSelectorEntry(Thread,Context.SegFs,SelEntry) then
    RaiseLastOSError;
  with SelEntry do
    FSBase:=(BaseHi shl 24) or (BaseMid shl 16) or SelEntry.BaseLow;
	if not VirtualProtectEx(Process,Ptr(FSBase),SizeOf(TEB),PAGE_READWRITE,@n) then
    RaiseLastOSError;
  if not ReadProcessMemory(Process,Ptr(FSBase),@TEB,SizeOf(TEB),n) then
    RaiseLastOSError;
	if not VirtualProtectEx(Process,TEB.Peb,SizeOf(PEB),PAGE_READWRITE,@n) then
    RaiseLastOSError;
  if not ReadProcessMemory(Process,TEB.Peb,@PEB,SizeOf(PEB),n) then
    RaiseLastOSError;
  Result:=TEB.Peb.ImageBaseAddress;                             //That's it, we now have the address at which the process is loaded
end;

function RedirectAPI(DllName:string;InitialVOffset,NewVOffset,ImageBase:Cardinal;ImageDosHeader:PImageDosHeader;ImageNTHeader:PImageNtHeaders):Boolean;
var
  ImportDirectory:Cardinal;
  ImageImportDescriptor:PImageImportDescriptor;
  ImageThunkData:PImageThunkData;
begin
  Result:=False;
  ImportDirectory:=ImageNTHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
  if ImportDirectory=0 then                                             // Well, I think we can find cases where there are no imported libraries...
    Exit;
  ImageImportDescriptor:=Ptr(ImageBase+ImportDirectory);
  while ImageImportDescriptor.Name<>0 do begin                          // Enumerate imported DLL's
    if ImageImportDescriptor.Misc.OriginalFirstThunk<>0 then
      ImageThunkData:=PImageThunkData(ImageBase+ImageImportDescriptor.Misc.OriginalFirstThunk)
    else
      ImageThunkData:=PImageThunkData(ImageBase+ImageImportDescriptor.FirstThunk);
    while ImageThunkData.AddressOfData<>nil do begin                    // Enumerate procs imported from the DLL
      if PCardinal(ImageThunkData)^=InitialVOffset then begin           // Found the imported proc!
        PCardinal(ImageThunkData)^:=NewVOffset;                         // Replace it and leave before Bill Gates notices anything!
        Result:=True;
//        Exit;  //Apparently, there can be more than one instance of the imported proc in the table. So let's continue just in case...
      end;
      Inc(ImageThunkData);
    end;
    Inc(ImageImportDescriptor);
  end;
end;

function ReplaceAPI(DllName:string;ProcName:string;NewProc:Pointer;Process,Thread:THandle):Boolean;
var
  ImageBase,OldProc,RedirectMem,LauncherCode:Pointer;
  ImageDosHeader:TImageDosHeader;
  ImageNTHeader:TImageNtHeaders;
  n,LauncherSize:Cardinal;
  Module:HModule;
begin
  if Process=0 then
    Process:=GetCurrentProcess;
  if Thread=0 then
    Thread:=GetCurrentThread
  else
    SuspendThread(Thread);
  try
    ImageBase:=GetImageBase(Process,Thread);
    if not ReadProcessMemory(Process,ImageBase,@ImageDosHeader,SizeOf(ImageDosHeader),n) then  // The DOS header is located at offset 0...
      RaiseLastOSError;
    if not ReadProcessMemory(Process,Ptr(Integer(ImageBase)+ImageDosHeader._lfanew),@ImageNTHeader,SizeOf(ImageNTHeader),n) then
      RaiseLastOSError;                                                                        // The NT is located at offset _lfanew...
    Module:=LoadLibrary(PChar(DllName));
    OldProc:=GetProcAddress(Module,PChar(ProcName));                                           // Get the real API address
    if not Assigned(OldProc) then
      Assert(False,'Wrong library or procedure name');                                         // TODO: use Result:=False;Exit;
    GetLauncherCode(LauncherCode,LauncherSize,NewProc);
    try
      RedirectMem:=VirtualAllocEx(Process,nil,LauncherSize,MEM_COMMIT,PAGE_EXECUTE_READWRITE); // Let's add some new code to the module    ;-)
      if not WriteProcessMemory(Process,RedirectMem,LauncherCode,LauncherSize,n) then          // Write the launcher code
        RaiseLastOSError;
      Result:=RedirectAPI(DllName,Cardinal(OldProc),Cardinal(RedirectMem),Cardinal(ImageBase),@ImageDosHeader,@ImageNTHeader); // OK, here we go, let's modify the headers
    finally
      FreeMem(LauncherCode);
    end;
  finally
    ResumeThread(Thread);
  end;
end;

end.

Conclusion :


Le code est de moi, je me suis un peu inspiré de cet article, même si ma méthode est un peu différente:
http://www.codeproject.com/useritems/inject2it.asp

Je préfère aussi prévenir tout de suite: CE N'EST PAS UN VIRUS donc pas la peine que les admins le désactivent. De toute façon j'ai changé le code pour qu'on ne puisse pas injecter du code dans un autre processus.

Le code est de niveau expert, son utilisation de niveau débutant, donc en moyenne initié...

Codes Sources

A voir également

Vous n'êtes pas encore membre ?

inscrivez-vous, c'est gratuit et ça prend moins d'une minute !

Les membres obtiennent plus de réponses que les utilisateurs anonymes.

Le fait d'être membre vous permet d'avoir un suivi détaillé de vos demandes et codes sources.

Le fait d'être membre vous permet d'avoir des options supplémentaires.