Getting StackOverflow exception while reading stringlist in newly hooked LoadResString method

683 views Asked by At

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.
1

There are 1 answers

6
fantaghirocco On BEST ANSWER

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.