函数

我们接下来会展示如何使用Frida来追踪函数,修改参数,在目标进程内调用函数。

配置实验环境

创建文件 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);
  }
}

使用下面的命令进行编译:

$ gcc -Wall hello.c -o hello

启动程序,然后记录下函数 f() 的地址 (本例子中地址为 0x400544 ):

f() is at 0x400544
Number: 0
Number: 1
Number: 2
…

拦截函数

下面的脚本展示了如何在目标进程内拦截函数,并将函数的参数返回。创建一个文件 hook.py:

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()

把前面你获得的地址作为参数,运行该脚本 (本例中为 0x400544 ):

$ python hook.py 0x400544

该脚本会每秒向你发送一个新的信息,类似下面这样:

{'type': 'send', 'payload': 531}
{'type': 'send', 'payload': 532}

修改函数参数

接下来: 我们想修改目标进程中函数接收到的参数。创建一个文件 modify.py:

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()

运行该脚本来注入hello 进程 (应该还在运行):

$ python modify.py 0x400544

此时,运行 hello process 的终端会停止计数,并一直输出 1337, 直到你输入 Ctrl-D 来解除注入.

Number: 1281
Number: 1282
Number: 1337
Number: 1337
Number: 1337
Number: 1337
Number: 1287
Number: 1288
Number: 1289
…

函数调用

我们可以使用Frida来调用目标进程中的函数。创建文件 call.py :

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()

运行该脚本:

$ python call.py 0x400544

然后仔细观察运行hello的终端:

Number: 1879
Number: 1911
Number: 1911
Number: 1911
Number: 1880
…

实验 2 - 字符串注入和函数调用

我们不仅能注入整形数,还可以注入字符串。而且如果你需要的话,你可以注入任何类型的对象。

先创建一个新文件 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);
  }
}

和前面类似,我们先创建一个脚本 stringhook.py, 该脚本使用 Frida 将字符串注入内存,然后以下面的方式来调用 f() 函数:

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()

仔细观察 hi 进程的输出, 你会看到类似下面的输出:

...
String: Testing!
String: Testing!
String: TESTMEPLZ!
String: Testing!
String: Testing!
...

使用类似的方法,比如 Memory.alloc()Memory.protect() 来修改 进程内存。 使用 python 的 ctypes 库来实现注入, 以及其他内存对象,比如可以创建 structs, 将其作为字节数组加载进内存,将后将其地址以指针的方式传递给函数。

恶意内存注入 - 案例: sockaddr_in 结构

任何一个做过网络编程编程的工程师都知道,C 中最常用的数据类型是 struct . 下面是一个原生的程序,该程序创建一个网络套接字,通过 5000 端口来连接服务器,然后通过建立的连接发送字符串 "Hello there!"

#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;
}

这是个相当标准的代码,该代码将 IP 地址作为第一个参数传入。 如果你在一个终端内运行命令 nc -lp 5000 并在另一个终端内运行 ./client 127.0.0.1, 然后,你应该在 netcat 内看到信息出现,而且可以在 client 内返回信息。

现在,我们可以做一些有趣的事情。我们前面介绍了字符串注入和指针注入。这里我们同样可以做类似的事情来修改结构 sockaddr_in ,程序运行时会将该结构分割成若干部分:

$ ./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

有很多的在线资源介绍了 struct 的内存结构,如果你不熟悉,你可以自行查阅。 本例中,重要的比特是字节 0x1388, 也就是 10进制的 5000. 而这正是我们的端口号(也就是十六进制下IP地址后的4个字节)。 如果我们将该值修改为 0x1389 , 我们就可以将客户端重定向到另一个端口。如果我们再修改接下来的4个字节,我们甚至可以完全的修改客户端访问的地址!

这里的脚本向内存内注入了恶意结构,然后劫持了 libc.so 内的 connect() 函数。

创建文件 struct_mod.py:

import frida
import sys

session = frida.attach("client")
script = session.create_script("""
// 首先,我们先申请一些内存来存储我们的结构体:
send('Allocating memory and writing bytes...');
const st = Memory.alloc(16);

// 现在我们用下面的数字来填出内存 —— 这看起来有点蠢,但是确实能运行...
st.writeByteArray([0x02, 0x00, 0x13, 0x89, 0x7F, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]);

// Module.getExportByName() 可以在不知道源码的前提下在模块内寻找到函数
// 不过有一点慢,尤其是在大型二进制文件里!(具体情况要具体分析)
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()

请注意,该脚本意在展示 Module.getExportByName() 接口可以在目标进程内 ,通过函数名来查询任何导出函数。如果我们提供了具体的模块,那么查找的速度会更快,尤其是在大型二进制文件上。不过这个不是本例子的重点。

现在运行 ./client 127.0.0.1, 在另一个终端里运行 nc -lp 5001, 然后在第三个终端里运行 ./struct_mod.py. 一旦我们的脚本运行起来,在运行 client 的终端里按下 ENTER, 然后 netcat 应该会显示 client 发送的字符串。

目前为止,我们已经成功劫持了最原生的网络连接。我们用 Frida 先注入了我们自己的数据对象,然后拦截了目标进程,以及使用 Interceptor 来修改函数。

本例子展示了 Frida 的强大:不需要修补操作,复杂的逆向,也不需要花费无尽的时间在汇编分析上。

这里有一个视频,展示了以上的代码例子:

https://www.youtube.com/watch?v=cTcM7R872Ls