Altering function behaviour

Hooking functions

At some point we may want to inspect certain function or method in the binary we are analyzing, f.e. looking at the parameters received or the returned value. Frida provides all the tools to inspect those elements just using JavaScript.

Lets see how can we view the parameters received by the function Process_compare in the module htop [2]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 Inteceptor.attach(Module.findExportByName('htop', 'Process_compare'), {
     onEnter: function(args){
         console.log('The function `Process_compare` was called');
         for(var i in args){
             console.log('The argument #' + i + ' is : ' + args[i]);
             // We can change any argument
             if(i == 2){
                 args[i] = NULL; // NULL is a shorthand for ptr('0x0)
             }
         }
         console.log('The return address is: ' + this.returnAddress);
         console.log('It\'s running on the thread #' + this.threadId);
         console.log('The errno is: ' + this.errno);
         // console.log('In Windows we should use ' + this.lastError + ' instead');
         console.log('We can also read registers:');
         console.log('rax: ' + this.context.rax);
         // this.context.pc and this.context.sp automatically handle if it is RIP/EIP/IP and RSP/ESP/SP respectively
         console.log('The instruction pointer is: ' + this.context.pc);
         console.log('Stack is at: ' + this.context.sp);
         // We can also save any value we need in the `this` object [#2]_
         this.arg1 = args[0].toInt32();
     },
     onComplete: function(retVal){
         // [#2] And use it later when the function finished before returning
         if(this.arg1 == 0xdeadbeef){
              retVal = 0;
         }
     }
 });

It is also possible to intercept an arbitrary address:

1
2
3
4
5
6
 Interceptor.attach(Module.findExportByName('htop', 'Process_compare').add(0x20), function(args){
     /* This function has the same signature as the `onEnter` callback
      * but in this case it is NOT a callback object instead we use a
      * plain function
      */
 });

Be aware that even if the function has the same signature as the onEnter callback the arguments may not be correctly shown if the code has moved the stack pointer or the values passed through the registers have been modified, it is up to you to properly handle this cases.

Attaching to functions may have a big impact in the performance as we are hooking the function and injecting calls to the JavaScript engine for the onEnter and onComplete callbacks, therefore it’s important to not add an onEnter or onComplete hook if it’s not needed (Don’t add the callback at all, avoid giving an empty function such as function(retVal){} as it will be called anyway)

Also, as it may impact the performance unhooking the functions as soon as possible is recommended:

1
2
3
4
5
6
7
8
 var to_hook = Module.findExportByName('htop', 'Process_compare');
 var hooked = Interceptor.attach(to_hook, {
     onEnter: function(args){
     /* Do the stuff */
     }
 });
 /* After we finished what we had to do */
 hooked.detach();

It’s also possible to clear all hooks at once

1
2
3
4
5
6
7
8
 var to_hook = Module.findExportByName('htop', 'Process_compare');
 Interceptor.attach(to_hook, {
     onEnter: function(args){
     /* Do the stuff */
     }
 });

 Interceptor.detahAll();

Overwrite functions

A different kind of hook can be used when we want to completely overwrite a function’s behaviour. As in the previous examples the new implementation is written in JavaScript:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
 /* We can use null as the module and it will look in all the available modules */
 var send_ptr = Module.findExportByName(null, 'send');

 /* We need to tell frida the function signature to be able to call it later */
 var send_function = new NativeFunction(send_ptr, 'uint', ['int', 'pointer', 'uint', 'int']);

 /* To implement a function we create a `NativeCallback` object */
 var my_implementation = new NativeCallback(
     /* Actual implementation */
     function(sockfd, buffer, size, flags){
         console.log('Intercepted message: ' + buffer.toString());
         return send_function(sockfd, buffer, size, flags); // Calling the original function is completely optional
     },
     'uint', // Return type
     ['int', 'pointer', 'uint', 'int'] // Parameters type
 );

 Interceptor.replace(send_ptr, my_implementation);

Flushing memory

As Frida is an asynchronous framework, changes in memory are not immediatly commited. Those changes are actually done in memory on each event loop. [3]

In most cases we won’t need to flush the memory by ourselves as we’ll be using Frida console or from a Python script. But in some cases, like for example if we configure Frida to directly load a script instead of stablishing a communication to a Python script or initializing a Frida server, we should make sure that our modifications are correctly flushed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 var send_ptr = Module.findExportByName(null, 'send');
 var send_function = new NativeFunction(send_ptr, 'uint', ['int', 'pointer', 'uint', 'int']);
 var my_implementation = new NativeCallback(
     function(sockfd, buffer, size, flags){
         console.log('Intercepted message: ' + buffer.toString());
         return send_function(sockfd, buffer, size, flags); // Calling the original function is completely optional
     },
     'uint', // Return type
     ['int', 'pointer', 'uint', 'int'] // Parameters type
 );
 Interceptor.replace(send_ptr, my_implementation);
 Interceptor.flush();
 send_function(4, 'test', 4, NULL);

In the previous example, if we weren’t calling Interceptor.flush the send function would probably hasn’t been replaced yet, so we will be calling the original function without our hook. To prevent this, we should manually flush the memory before calling the function we have just replaced.

footnotes

[2]The program itself is also a module that exports all its functions and imports all other modules.
[3]In case you are not used to asynchronous programming it doesn’t matter how it works internally. You should just know that the event loop is completed once the Frida library calls send or the JavaScript runtime is about to leave (a.k.a. end of the script)