Exploring Native Functions with Frida on Android — part 4

Modifying the input arguments and return values of native functions.

The Mobile Security Guys
The Startup
5 min readJun 2, 2020

--

We’ve observed how modules from both app execution and the app lifecycle are loaded within the app, but why would this need to be done on a pentest? What can we actually do with all the scripts?

The aim of this final post is to solve the third consideration; to push the scripts even further to manipulate the input arguments and return values of native functions to be able to modify the true workflow of the app and its designed behaviour.

The Target

Let us describe our scenario, and our goal. We are targetting the OpenSSL function named SSL_CTX_set_cipher_list() which essentially is used to specify the default ciphers to be used during the SSL negotiation.

Considering this final example is the most complex one shown and described, reference to the actual code of the OpenSSL function are used. This code is available at: https://www.openssl.org/source/old/1.0.2/

So why this function?

1. It takes 2 input arguments representing pointers, an SSL_CTX struct (similar to Java object representing SSL context) and char (array of characters representing a string)

2. It returns an int value that can be either 0 or 1; the same as a true/false Boolean in Java. In this case returning 0 would mean false, therefore the function returns with an error, otherwise it will return 1.

3. If the circumstances were correct, manipulating this function could be misused to lead to a FREAK¹ attack to downgrade the cipher to force weaker encryption, and represents an interesting case study to test vulnerable backends.

Replacing Arguments of the Function

Having studied and understood what the target function does, the idea with our script will be to attempt to force a downgrade of the default ciphers used inside the app. This is accomplished by modifying the second function argument (args[1]), the char array representing a string with following string: “ALL:EXP” to represent all ciphers including weak ones.

To accomplish this, we will make use of NativeFunction². Simply put, NativeFunction lets us call a specified function inside the target process.
It requires us to provide the memory address of the desired function as its first argument, which can be achieved using NativePointer³, (ptr for brevity), then we need to follow the target functions signature in terms of return and argument types, respectively arguments 2 and 3, in this case we know the function returns an int and it takes inputs as pointer.

Target function signature:

int <<return value>> SSL_CTX_set_cipher_list(SSL_CTX *ctx <<arg1>>, const char *str <<arg2>>)                    

Therefore, we can use the following instantiation in our script:

var mySet_cipher_list = new NativeFunction(ptr(myExportAddr), 'int’, [‘pointer’,’pointer’]);

At this stage we just need to call our function variable:

onEnter is passed the target arguments internally, so this is just called simply with function(args).
We also need to call our function inside onEnter so we can be sure it is called every time. Printing out the value returned by our function allows us to test it actually worked.

onEnter: function(args)
var ret = mySet_cipher_list(args[0],str);
console.log("return value " + ret);
console.log("decoded args[1] " + Memory.readCString(args[1]));

Replacing Return Values of the Function

Having explained and illustrated how to replace arguments of a function, we can utilise the same techniques to modify return values. In this case of theorising the FREAK attack it is not required as we weren’t interested in what was returned, but what was being input.

However, let us contemplate a case where hooking a native function performs a check and returns a Boolean value indicating whether the check has passed (return 1) or not (return 0), and on failure the app would perhaps stop functioning or show an error.

Root detection strings checking for Magisk stored in a .so file

A good example of this would be native backed root detection, whereby the checks to be made; checking for ‘su’ or ‘superuser’, checking for known root package names or applications are stored as strings within a .so file.

We can use this technique to bypass root detection checks which return a Boolean.

To accomplish this, we need to add some code inside the onLeave callback:

Just like the args value passed to onEnter, the retval passed to onLeave is done internally by Frida.
As we are working with integers, we use the .replace(intVal) to actually replace the returned value, in this case, with 0. A couple of log statements will test that our original value has in fact been modified.

onLeave: function(retval)
console.log("supposed val " + retval);
retval.replace(0);
console.log("new val " + retval);

The full script against an app using the OpenSSL library is provided below along with the output generated from it. At the end of the output both the original return value, referred as “supposed”, and after we have modified it, “replaced”, can be observed.

It is worth noting that printing the args using JSON.stringify() will print the actual pointer value of the memory address; however using the Memory API allows us to examine the actual content.
Similarly, the script output has occurred multiple times, this is due to not only the actual function, SSL_CTX_set_cipher_list being called multiple times by the app, but also due to our modification making it retry until successful.

Script for locating a specific function within a module and modifying the input arguments, and output return values
Completed script for locating a specific function within a module and modifying the input arguments, and output return values.
Output from the script, showing the callbacks of the function and the values.

We can only “touch, tamper and modify” our C functions rather than creating entire new ones like in Java.

A final consideration of this technique that should be kept in mind when hooking a C function, we cannot use the same approach one may use when hooking a standard Java function.
We cannot hook the function like a Java method and do whatever we want, for example implementing a new TrustManager. When hooking C functions, we can only “touch, tamper, modify” what goes into the function before it is executed and what comes out of it after the function is executed. Therefore, we need to adapt our approaches to keep this logic in mind when examining these native libraries and manipulating them in a real world setting.

--

--

The Mobile Security Guys
The Startup

A diverse collection of posts on mobile and API-based security and testing techniques, contributed by a group of mobile professionals in our spare time.