函数

本页为社区译文;如有疑义,请以英文原文为准。 英文原文

本文介绍如何使用 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 函数

以下脚本演示如何 hook 目标进程内部的函数调用,并将一个函数参数报告给你。创建文件 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

如果不完全熟悉结构体的布局,网上有很多资料可以解释各部分的含义。这里的重要内容是字节 0x1388,即十进制的 5000。这是端口号(紧随其后的 4 个字节是十六进制 IP 地址)。如果把它改为 0x1389,就能将客户端重定向到其他端口。如果修改接下来的 4 个字节,则可以彻底改变客户端所指向的 IP 地址!

下面的脚本会把恶意结构体注入内存,然后劫持 libc.so 中的 connect() 函数,让它使用新结构体作为参数。

按如下内容创建文件 struct_mod.py:

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.getGlobalExportByName() can find functions without knowing the source
// module, but it's slower, especially over large binaries! YMMV...
Interceptor.attach(Module.getGlobalExportByName('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.getGlobalExportByName() API,按名称查找目标中的任何导出函数。如果能够提供模块,在较大的二进制文件上速度会更快,但这里并不十分关键。

现在运行 ./client 127.0.0.1,在另一个终端运行 nc -lp 5001,再在第三个终端运行 ./struct_mod.py。脚本运行后,在 client 终端窗口按 ENTER,netcat 此时应显示客户端发送的字符串。

我们已经成功劫持了原始网络通信:将自定义数据对象注入内存,用 Frida hook 进程,并借助 Interceptor 操作函数。

这体现了 Frida 的真正威力——无需打补丁,无需复杂的逆向工作,也无需无休止地盯着反汇编代码苦熬数小时。

下面是一段演示上述过程的简短视频:

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