Functions
We show how to use Frida to inspect functions as they are called, modify their arguments, and do custom calls to functions inside a target process.
Setting up the experiment
Create a file hello.c
:
#include <stdio.h>
#include <unistd.h>
void
f (int n)
{
printf ("Number: %d\n", n);
}
int
main (int argc,
char * argv[])
{
int i = 0;
printf ("f() is at %p\n", f);
while (1)
{
f (i++);
sleep (1);
}
}
Compile with:
$ gcc -Wall hello.c -o hello
Start the program and make note of the address of f()
(0x400544
in the
following example):
f() is at 0x400544
Number: 0
Number: 1
Number: 2
…
Hooking Functions
The following script shows how to hook calls to functions inside a target
process and report back a function argument to you. Create a file hook.py
containing:
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter(args) {
send(args[0].toInt32());
}
});
""" % int(sys.argv[1], 16))
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
Run this script with the address you picked out from above (0x400544
on our
example):
$ python hook.py 0x400544
This should give you a new message every second on the form:
{'type': 'send', 'payload': 531}
{'type': 'send', 'payload': 532}
…
Modifying Function Arguments
Next up: we want to modify the argument passed to a function inside a target
process. Create the file modify.py
with the following contents:
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter(args) {
args[0] = ptr("1337");
}
});
""" % int(sys.argv[1], 16))
script.load()
sys.stdin.read()
Run this against the hello
process (which should be still running):
$ python modify.py 0x400544
At this point, the terminal running the hello process
should stop counting
and always report 1337
, until you hit Ctrl-D
to detach from it.
Number: 1281
Number: 1282
Number: 1337
Number: 1337
Number: 1337
Number: 1337
Number: 1287
Number: 1288
Number: 1289
…
Calling Functions
We can use Frida to call functions inside a target process. Create the file
call.py
with the contents:
import frida
import sys
session = frida.attach("hello")
script = session.create_script("""
const f = new NativeFunction(ptr("%s"), 'void', ['int']);
f(1911);
f(1911);
f(1911);
""" % int(sys.argv[1], 16))
script.load()
Run the script:
$ python call.py 0x400544
and keep a watchful eye on the terminal (still) running hello
:
Number: 1879
Number: 1911
Number: 1911
Number: 1911
Number: 1880
…
Experiment No. 2 - Injecting Strings and Calling a Function
Injecting integers is really useful, but we can also inject strings, and indeed, any other kind of object you would require for fuzzing/testing.
Create a new file hi.c
:
#include <stdio.h>
#include <unistd.h>
int
f (const char * s)
{
printf ("String: %s\n", s);
return 0;
}
int
main (int argc,
char * argv[])
{
const char * s = "Testing!";
printf ("f() is at %p\n", f);
printf ("s is at %p\n", s);
while (1)
{
f (s);
sleep (1);
}
}
In a similar way to before, we can create a script stringhook.py
, using Frida
to inject a string into memory, and then call the function f() in the following
way:
import frida
import sys
session = frida.attach("hi")
script = session.create_script("""
const st = Memory.allocUtf8String("TESTMEPLZ!");
const f = new NativeFunction(ptr("%s"), 'int', ['pointer']);
// In NativeFunction param 2 is the return value type,
// and param 3 is an array of input types
f(st);
""" % int(sys.argv[1], 16))
def on_message(message, data):
print(message)
script.on('message', on_message)
script.load()
Keeping a beady eye on the output of hi
, you should see something along these
lines:
...
String: Testing!
String: Testing!
String: TESTMEPLZ!
String: Testing!
String: Testing!
...
Use similar methods, like Memory.alloc()
and Memory.protect()
to manipulate
the process memory with ease. Couple this with the python ctypes
library, and
other memory objects, like structs
can be created, loaded as byte arrays, and
then passed into functions as pointer arguments.
Injecting Malicious Memory Objects - Example: sockaddr_in struct
Anyone who has done network programming knows that one of the most commonly
used data types is the struct
in C. Here is a naive example of a program
that creates a network socket, and connects to a server over port 5000, and
announces itself by sending the string "Hello there!"
over the connection.
#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
int
main (int argc,
char * argv[])
{
int sock_fd, i, n;
struct sockaddr_in serv_addr;
unsigned char * b;
const char * message;
char recv_buf[1024];
if (argc != 2)
{
fprintf (stderr, "Usage: %s <ip of server>\n", argv[0]);
return 1;
}
printf ("connect() is at: %p\n", connect);
if ((sock_fd = socket (AF_INET, SOCK_STREAM, 0)) < 0)
{
perror ("Unable to create socket");
return 1;
}
bzero (&serv_addr, sizeof (serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons (5000);
if (inet_pton (AF_INET, argv[1], &serv_addr.sin_addr) <= 0)
{
fprintf (stderr, "Unable to parse IP address\n");
return 1;
}
printf ("\nHere's the serv_addr buffer:\n");
b = (unsigned char *) &serv_addr;
for (i = 0; i != sizeof (serv_addr); i++)
printf ("%s%02x", (i != 0) ? " " : "", b[i]);
printf ("\n\nPress ENTER key to Continue\n");
while (getchar () == EOF && ferror (stdin) && errno == EINTR)
;
if (connect (sock_fd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) < 0)
{
perror ("Unable to connect");
return 1;
}
message = "Hello there!";
if (send (sock_fd, message, strlen (message), 0) < 0)
{
perror ("Unable to send");
return 1;
}
while (1)
{
n = recv (sock_fd, recv_buf, sizeof (recv_buf) - 1, 0);
if (n == -1 && errno == EINTR)
continue;
else if (n <= 0)
break;
recv_buf[n] = 0;
fputs (recv_buf, stdout);
}
if (n < 0)
{
perror ("Unable to read");
}
return 0;
}
This is fairly standard code, and calls out to any IP address given as the
first argument. If you run nc -lp 5000
and in another terminal window run
./client 127.0.0.1
, you should see the message appear in netcat, and also
be able to send messages back to client
in return.
Now, we can start having some fun - as we saw above, we can inject strings and
pointers into the process. We can do the same by manipulating the struct
sockaddr_in
which the program spits out as part of its operation:
$ ./client 127.0.0.1
connect() is at: 0x400780
Here's the serv_addr buffer:
02 00 13 88 7f 00 00 01 30 30 30 30 30 30 30 30
Press ENTER key to Continue
If you are not fully familiar with the structure of a struct, there are many
resources online that will tell you what’s what. The important bits here are the
bytes 0x1388
, or 5000 in dec. This is our port number (the 4 bytes that
follow are the IP address in hex). If we change this to 0x1389
then we can
re-direct our client to a different port. If we change the next 4 bytes we
can change the IP address that the client points at completely!
Here’s a script to inject the malicious struct into memory, and then hijack the
connect()
function in libc.so
to take our new struct as its argument.
Create the file struct_mod.py
as follows:
import frida
import sys
session = frida.attach("client")
script = session.create_script("""
// First, let's give ourselves a bit of memory to put our struct in:
send('Allocating memory and writing bytes...');
const st = Memory.alloc(16);
// Now we need to fill it - this is a bit blunt, but works...
st.writeByteArray([0x02, 0x00, 0x13, 0x89, 0x7F, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]);
// Module.getExportByName() can find functions without knowing the source
// module, but it's slower, especially over large binaries! YMMV...
Interceptor.attach(Module.getExportByName(null, 'connect'), {
onEnter(args) {
send('Injecting malicious byte array:');
args[1] = st;
}
//, onLeave(retval) {
// retval.replace(0); // Use this to manipulate the return value
//}
});
""")
# Here's some message handling..
# [ It's a little bit more meaningful to read as output :-D
# Errors get [!] and messages get [i] prefixes. ]
def on_message(message, data):
if message['type'] == 'error':
print("[!] " + message['stack'])
elif message['type'] == 'send':
print("[i] " + message['payload'])
else:
print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()
Note that this script demonstrates how the Module.getExportByName()
API can
be used to find any exported function by name in our target. If we can supply a
module then it will be faster on larger binaries, but that is less critical
here.
Now, run ./client 127.0.0.1
, in another terminal run nc -lp 5001
, and in a
third terminal run ./struct_mod.py
. Once our script is running, press ENTER
in the client
terminal window, and netcat should now show the string sent
by the client.
We have successfully hijacked the raw networking by injecting our own data
object into memory and hooking our process with Frida, and using Interceptor
to do our dirty work in manipulating the function.
This shows the real power of Frida - no patching, complicated reversing, nor difficult hours spent staring at dissassembly without end.
Here’s a quick video demonstrating the above:
https://www.youtube.com/watch?v=cTcM7R872Ls