delphiwinapihookresourcestring

Getting StackOverflow exception while reading stringlist in newly hooked LoadResString method


For reference - following is my code, where I am getting StackOverflow exception in NewLoadResString function. The case is like I have creted two stringlist i.e. RecStrNameIdMap and NewStringValueList. Here RecStrNameIdMap is hash string list to store name and string Identifier mapping. So that I can refer Resource string name for its Identifier i.e ID.

NewStringValueList is a string list which contains new values for few of the Resourcestrings.

I have hooked up NewLoadResString method on system.LoadResString method. In new method I am checking if I have new value for given resourcestring in NewStringValueList then get that value and return new instead of old declared value.

Stack Overflow exception occurs on line *

if RecStrNameIdMap.IndexOfName(IntToStr(ResStringRec^.Identifier)) > -1 then

* Can anyone please check why I am getting this error.

unit UnitTest;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, IniFiles, StdCtrls;

type
  TForm2 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;


type
  TMethodHook = class
  private
    aOriginal : packed array[ 0..4 ] of byte;
    pOldProc, pNewProc : pointer;
    pPosition : PByteArray;
  public
    constructor Create( pOldProc, pNewProc : pointer );
    destructor Destroy; override;
  end;


var
  Form2: TForm2;

implementation

{$R *.dfm}

ResourceString
  RS_1 = 'ABC';
  RS_2 = 'XYZ';

procedure TForm2.Button1Click(Sender: TObject);
var
  aMethodHook: TMethodHook;
  RecStrNameIdMap: THashedStringList;
  NewStringValueList: TStringList;

  {Hookup aNewProcedure on aOriginalProcedure}
  procedure RegisterProcedures(aOriginalProcedure, aNewProcedure: pointer);
  begin
    if Assigned(aOriginalProcedure) and Assigned(aNewProcedure) then
      aMethodHook := TMethodHook.Create( aOriginalProcedure, aNewProcedure);
  end;

  {Replacement for System.LoadResString}
  function NewLoadResString(ResStringRec: PResStringRec): String;
  var
    Buffer: array [0..4095] of char;
  begin
    if ResStringRec = nil then Exit;
    if ResStringRec.Identifier >= 64 * 1024 then
    begin
      Result := PChar(ResStringRec.Identifier);
    end
    else
    begin
      if RecStrNameIdMap.IndexOfName(IntToStr(ResStringRec^.Identifier)) > -1 then
      begin
        Result := NewStringValueList.Values[
          RecStrNameIdMap.Values[IntToStr(ResStringRec^.Identifier)]];
      end
      else
      begin
        SetString(Result, Buffer,
          LoadString(FindResourceHInstance(ResStringRec.Module^),
            ResStringRec.Identifier, Buffer, SizeOf(Buffer)));
      end;
    end;
  end;

  procedure CreateNameIdMapping;
  begin
    {This is done to get string name from ID}
    RecStrNameIdMap.CaseSensitive := False;
    RecStrNameIdMap.Add(Inttostr(PResStringRec(RS_2)^.Identifier)+'='+'XYZ');
  end;

begin
  aMethodHook := nil;
  try
    RecStrNameIdMap := THashedStringList.Create;
    NewStringValueList := TStringList.Create;

    CreateNameIdMapping;

    {Create new value list for ResourceStrings}
    NewStringValueList.Add('XYZ'+'='+'new value for ResourceString RS_2');
    RegisterProcedures(@System.LoadResString, @NewLoadResString);

    {This should return 'new value for ResourceString RS_2' instead of 'XYZ'}
    ShowMessage(RS_2);

    {This should return 'ABC' - no change in value}
    ShowMessage(RS_1);
  finally
    aMethodHook.Free;
    RecStrNameIdMap.Free;
    NewStringValueList.Free;
  end;
end;

{ TMethodHook }

constructor TMethodHook.Create(pOldProc, pNewProc: pointer);
var
  iOffset : integer;
  iMemProtect : cardinal;
  i : integer;
begin
  Self.pOldProc := pOldProc;
  Self.pNewProc := pNewProc;

  pPosition := pOldProc;
  iOffset := integer( pNewProc ) - integer( pointer( pPosition ) ) - 5;

  for i := 0 to 4 do aOriginal[ i ] := pPosition^[ i ];

  VirtualProtect( pointer( pPosition ), 5, PAGE_EXECUTE_READWRITE,
    @iMemProtect );

  pPosition^[ 0 ] := $E9;
  pPosition^[ 1 ] := byte( iOffset );
  pPosition^[ 2 ] := byte( iOffset shr 8 );
  pPosition^[ 3 ] := byte( iOffset shr 16 );
  pPosition^[ 4 ] := byte( iOffset shr 24 );
end;

destructor TMethodHook.Destroy;
var
  i : integer;
begin
  for i := 0 to 4 do pPosition^[ i ] := aOriginal[ i ];
  inherited;
end;

end.

Solution

  • It seems that the replacing procedure cannot be a nested routine.
    As stated in the documentation:

    Procedural types allow you to treat procedures and functions as values that can be assigned to variables or passed to other procedures and functions.

    ...

    Nested procedures and functions (routines declared within other routines) cannot be used as procedural values, nor can predefined procedures and functions.

    A procedural type is a Pointer. While a nested routine cannot be used as procedural type, I assume that a Pointer to a nested routine cannot be used as a procedure parameter or this action may have unpredictable result like in this case.
    The procedure is hooked correctly (you did); I extracted the procedure NewLoadResString and the stackoverflow error does not happen anymore.
    The resourcestring which pops up is always the old one but I made no changes in the NewLoadResString procedure.
    The entire edited unit follows.

    unit UnitTest;
    
    interface
    
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, IniFiles, StdCtrls;
    
    type
      TForm2 = class(TForm)
        Button1: TButton;
        procedure Button1Click(Sender: TObject);
      private
        { Private declarations }
        RecStrNameIdMap: THashedStringList;
        NewStringValueList: TStringList;
      public
        { Public declarations }
      end;
    
    
    type
      TMethodHook = class
      private
        aOriginal : packed array[ 0..4 ] of byte;
        pOldProc, pNewProc : pointer;
        pPosition : PByteArray;
      public
        constructor Create( pOldProc, pNewProc : pointer );
        destructor Destroy; override;
      end;
    
    
    var
      Form2: TForm2;
    
    implementation
    
    {$R *.dfm}
    
    ResourceString
      RS_1 = 'ABC';
      RS_2 = 'XYZ';
    
    
    {Replacement for System.LoadResString}
    function NewLoadResString(ResStringRec: PResStringRec): String;
    var
      Buffer: array [0..4095] of char;
    begin
      if ResStringRec = nil then Exit;
      if ResStringRec.Identifier >= 64 * 1024 then
      begin
        Result := PChar(ResStringRec.Identifier);
      end
      else
      begin
        if RecStrNameIdMap.IndexOfName(IntToStr(ResStringRec^.Identifier)) > -1 then
        begin
          Result := NewStringValueList.Values[
            RecStrNameIdMap.Values[IntToStr(ResStringRec^.Identifier)]];
        end
        else
        begin
          SetString(Result, Buffer,
            LoadString(FindResourceHInstance(ResStringRec.Module^),
              ResStringRec.Identifier, Buffer, SizeOf(Buffer)));
        end;
      end;
    end;
    
    procedure TForm2.Button1Click(Sender: TObject);
    var
      aMethodHook: TMethodHook;
    
      {Hookup aNewProcedure on aOriginalProcedure}
      procedure RegisterProcedures(aOriginalProcedure, aNewProcedure: pointer);
      begin
        if Assigned(aOriginalProcedure) and Assigned(aNewProcedure) then
          aMethodHook := TMethodHook.Create( aOriginalProcedure, aNewProcedure);
      end;
    
      procedure CreateNameIdMapping;
      begin
        {This is done to get string name from ID}
        RecStrNameIdMap.CaseSensitive := False;
        RecStrNameIdMap.Add(Inttostr(PResStringRec(RS_2)^.Identifier)+'='+'XYZ');
      end;
    
    begin
      aMethodHook := nil;
      RecStrNameIdMap := THashedStringList.Create;
      NewStringValueList := TStringList.Create;
      try
        CreateNameIdMapping;
    
        {Create new value list for ResourceStrings}
        NewStringValueList.Add('XYZ'+'='+'new value for ResourceString RS_2');
        RegisterProcedures(@System.LoadResString, @NewLoadResString);
    
        {This should return 'new value for ResourceString RS_2' instead of 'XYZ'}
        ShowMessage(RS_2);
    
        {This should return 'ABC' - no change in value}
        ShowMessage(RS_1);
      finally
        aMethodHook.Free;
        RecStrNameIdMap.Free;
        NewStringValueList.Free;
      end;
    end;
    
    { TMethodHook }
    
    constructor TMethodHook.Create(pOldProc, pNewProc: pointer);
    var
      iOffset : integer;
      iMemProtect : cardinal;
      i : integer;
    begin
      Self.pOldProc := pOldProc;
      Self.pNewProc := pNewProc;
    
      pPosition := pOldProc;
      iOffset := integer( pNewProc ) - integer( pointer( pPosition ) ) - 5;
    
      for i := 0 to 4 do aOriginal[ i ] := pPosition^[ i ];
    
      VirtualProtect( pointer( pPosition ), 5, PAGE_EXECUTE_READWRITE,
        @iMemProtect );
    
      pPosition^[ 0 ] := $E9;
      pPosition^[ 1 ] := byte( iOffset );
      pPosition^[ 2 ] := byte( iOffset shr 8 );
      pPosition^[ 3 ] := byte( iOffset shr 16 );
      pPosition^[ 4 ] := byte( iOffset shr 24 );
    end;
    
    destructor TMethodHook.Destroy;
    var
      i : integer;
    begin
      for i := 0 to 4 do pPosition^[ i ] := aOriginal[ i ];
      inherited;
    end;
    
    end.