Cross-Platform Code Hooking
July 26, 2017 Erik van Bilsen
July 26, 2017 Erik van Bilsen
[SHOWTOGROUPS=4,20]
Code hooks can be useful to add instrumentation and debug code, or to change the behavior of functions you don’t have the source code for. We take a look at how you can implement these in a way that works on all platforms that are currently supported by Delphi.
Hooking into existing code at run-time is frowned upon by many people since it can be used to create malicious code. But there are some legitimate uses of hooking as well. For example, in the not-too-distant future, we at Grijjy will present our cross-platform remote logging library that can be used to send log messages from any platform and view them in a log viewer on your PC. One of the features of this viewer is that it not only shows log messages, but it also provides a view of all live objects in your application as a list of class names and the current number of live instances of those classes. This information can be very useful to track down memory leaks and other memory related problems. For example, if the instance count of a certain kind of object keeps growing, but you expect it to shrink, then you may have forgotten to free an object somewhere, or you may have a reference cycle when running on an ARC platform or using object interfaces.
Instance Tracking
To implement this feature, we must somehow be able to get notified whenever an object is created or destroyed. We do this by hooking into the TObject.NewInstance and TObject.FreeInstance methods. These are the methods where memory for an object is actually (de)allocated. Inside those hooked methods, we duplicate the original implementations of these methods, and in addition update a global list of active instances. In this article, we show how we did this. It is accompanied by sample code in our Для просмотра ссылки Войдиили Зарегистрируйся repository on GitHub, in the directory Для просмотра ссылки Войди или Зарегистрируйся. You will find two sample applications that show a list of running instances. One is a FireMonkey application that runs on all Platforms except Linux. The other one is a console application that works on all desktop platforms, including Linux. These are some screen shots of the end result:
Hooking Methods
There are various ways you can hook into existing code at run-time. Unfortunately, I have not found a single method that works on all platforms. So our logging library, and the sample code for this article, uses one of two methods, depending on platform.
The first method I simply call Function Hooking. This works by overwriting the existing implementation of a function with a jump to a custom implementation. Unfortunately, this method does not work an iOS and Android since those platforms don’t allow you to overwrite executable code.
The second method is called Virtual Method Table (VMT) patching. This method is more limited than the first one, but also works on iOS and Android, but interestingly enough does not work on macOS.
Function Hooking
Function hooking works by overwriting the first few bytes of a function with a JMP instruction to a new function. In Delphi pseudo-code, this would look something like this:
Here, the first line of code is replaced with a goto instruction to our hooked version. In reality, this means overwriting the first 5 bytes of the function with an assembly JMP instruction.
Our goal is to create a function that we can call like this:
This redirects the implementation of TObject.NewInstance to our own HookedObjectNewInstance function. This function looks like this:
There are a few things to note here:
First it calls UntrackInstance to remove the instance from the hash map. After that follows the original implementation of TObject.FreeInstance. Note that this is a “regular” method, so the implicit Self parameter is a TObject, not a TClass.
[/SHOWTOGROUPS]
Code hooks can be useful to add instrumentation and debug code, or to change the behavior of functions you don’t have the source code for. We take a look at how you can implement these in a way that works on all platforms that are currently supported by Delphi.
Hooking into existing code at run-time is frowned upon by many people since it can be used to create malicious code. But there are some legitimate uses of hooking as well. For example, in the not-too-distant future, we at Grijjy will present our cross-platform remote logging library that can be used to send log messages from any platform and view them in a log viewer on your PC. One of the features of this viewer is that it not only shows log messages, but it also provides a view of all live objects in your application as a list of class names and the current number of live instances of those classes. This information can be very useful to track down memory leaks and other memory related problems. For example, if the instance count of a certain kind of object keeps growing, but you expect it to shrink, then you may have forgotten to free an object somewhere, or you may have a reference cycle when running on an ARC platform or using object interfaces.
Instance Tracking
To implement this feature, we must somehow be able to get notified whenever an object is created or destroyed. We do this by hooking into the TObject.NewInstance and TObject.FreeInstance methods. These are the methods where memory for an object is actually (de)allocated. Inside those hooked methods, we duplicate the original implementations of these methods, and in addition update a global list of active instances. In this article, we show how we did this. It is accompanied by sample code in our Для просмотра ссылки Войди
Hooking Methods
There are various ways you can hook into existing code at run-time. Unfortunately, I have not found a single method that works on all platforms. So our logging library, and the sample code for this article, uses one of two methods, depending on platform.
The first method I simply call Function Hooking. This works by overwriting the existing implementation of a function with a jump to a custom implementation. Unfortunately, this method does not work an iOS and Android since those platforms don’t allow you to overwrite executable code.
The second method is called Virtual Method Table (VMT) patching. This method is more limited than the first one, but also works on iOS and Android, but interestingly enough does not work on macOS.
Function Hooking
Function hooking works by overwriting the first few bytes of a function with a JMP instruction to a new function. In Delphi pseudo-code, this would look something like this:
Here, the first line of code is replaced with a goto instruction to our hooked version. In reality, this means overwriting the first 5 bytes of the function with an assembly JMP instruction.
You might wonder if you are even allowed to modify existing code this way. Normally, you cannot, because memory pages with executable code are read-only by default, and trying to modify them will result in an Access Violation. However, with the help of the VirtualProtect API on Windows, and the mprotect API on other (Posix) platforms, you can change the access level of those memory pages. Actually, you are only allowed to do that on Windows, macOS, iOS Simulator and Linux. For iOS and Android, we use a different approach (as presented later).Since this method of code hooking only works in Intel CPU’s, we don’t have to take an ARM version into account.
Our goal is to create a function that we can call like this:
1 | HookCode(@TObject.NewInstance, @HookedObjectNewInstance); |
1 2 3 4 5 6 7 8 9 10 11 12 | function HookedObjectNewInstance(const Self: TClass): TObject; var Instance: Pointer; begin GetMem(Instance, Self.InstanceSize); Result := Self.InitInstance(Instance); {$IFDEF AUTOREFCOUNT} TObjectOpener(Result).FRefCount := 1; {$ENDIF} TrackInstance(Result); end; |
- TObject.NewInstance is a (non-static) class method. Like regular methods, these methods have an implicit Self parameter. But in the case of class methods, this Self parameter refers to the class, and not to the instance. This is a Delphi language feature that you don’t see much in other object-oriented programming languages, and allows for powerful features like virtual class methods (which NewInstance is). In our hooked functions, we need to make any implicit Self parameters explicit, as we did in the example above.
- The majority of the implementation is just a copy of the original TObject.NewInstance method. We just need to use the Self parameter explicitly here to access its methods. Also, TObjectOpener is the common “hack” used to access protected fields and methods of a class.
- The last line is were we added our custom code. In this case, it calls a TrackInstance routine which adds the object’s class to a hasp map of running instances. I will not show the implementation of this routine here, since that is outside the scope of this article. You can look it up in the sample code on GitHub. One thing to note though is that multiple threads may be creating objects at the same time, so access to the list of instances must be protected with a lock.
1 2 3 4 5 6 7 | procedure HookedObjectFreeInstance(const Self: TObject); begin UntrackInstance(Self); Self.CleanupInstance; FreeMem(Pointer(Self)); end; |
You may wonder if there are better ways to execute the original code, other than to copy its implementation as we did here. There are. One of the ways is by using a library like Microsoft’s Detours. Way back in 2004, I wrote an article in The Delphi Magazine that presented a Delphi version of this library. It would copy part of the original implementation to a so-called “Trampoline” function. Then you would just call that trampoline function to execute the original code. However, using a library like Detours here is overkill, since the hooked methods are small and have not changed in many years. You can find a more recent version of Для просмотра ссылки Войдиили Зарегистрируйся on GitHub. Another well-known Delphi hooking library that provides similar functionality is Для просмотра ссылки Войдиили Зарегистрируйся. These libraries are Windows-only though…
[/SHOWTOGROUPS]