墨锋
Published on 2025-03-23 / 11 Visits
0
0

[翻译]FRIDA官方文档-教程-函数

函数

我们展示如何使用Frida来检查函数调用、修改函数参数,并在目标进程内对函数进行自定义调用。

设置实验

创建一个文件 hello.c

 #include <stdio.h>
 #include <unistd.h>
 ​
 void
 f (int n)
 {
   printf ("数字: %d\n", n);
 }
 ​
 int
 main (int argc,
       char * argv[])
 {
   int i = 0;
 ​
   printf ("f() 位于 %p\n", f);
 ​
   while (1)
   {
     f (i++);
     sleep (1);
   }
 }

编译:

 $ gcc -Wall hello.c -o hello

启动程序并记下 f() 的地址(以下示例中为 0x400544):

 f() 位于 0x400544
 数字: 0
 数字: 1
 数字: 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 进程的终端应停止计数并始终报告 1337,直到你按下 Ctrl-D 分离它。

数字: 1281
数字: 1282
数字: 1337
数字: 1337
数字: 1337
数字: 1337
数字: 1287
数字: 1288
数字: 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 的终端:

数字: 1879
数字: 1911
数字: 1911
数字: 1911
数字: 1880
…

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

注入整数非常有用,但我们也可以注入字符串,实际上,你可以注入任何其他类型的对象以进行模糊测试/测试。

创建一个新文件 hi.c

#include <stdio.h>
#include <unistd.h>

int
f (const char * s)
{
  printf ("字符串: %s\n", s);
  return 0;
}

int
main (int argc,
      char * argv[])
{
  const char * s = "Testing!";

  printf ("f() 位于 %p\n", f);
  printf ("s 位于 %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']);
    // 在NativeFunction中,参数2是返回值类型,
    // 参数3是输入类型的数组
f(st);
""" % int(sys.argv[1], 16))
def on_message(message, data):
    print(message)
script.on('message', on_message)
script.load()

密切关注 hi 的输出,你应该会看到类似以下的内容:

...
字符串: Testing!
字符串: Testing!
字符串: TESTMEPLZ!
字符串: Testing!
字符串: 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, "用法: %s <服务器IP>\n", argv[0]);
     return 1;
   }
 ​
   printf ("connect() 位于: %p\n", connect);
 ​
   if ((sock_fd = socket (AF_INET, SOCK_STREAM, 0)) < 0)
   {
     perror ("无法创建套接字");
     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, "无法解析IP地址\n");
     return 1;
   }
   printf ("\n这是serv_addr缓冲区:\n");
   b = (unsigned char *) &serv_addr;
   for (i = 0; i != sizeof (serv_addr); i++)
     printf ("%s%02x", (i != 0) ? " " : "", b[i]);
 ​
   printf ("\n\n按ENTER键继续\n");
   while (getchar () == EOF && ferror (stdin) && errno == EINTR)
     ;
 ​
   if (connect (sock_fd, (struct sockaddr *) &serv_addr, sizeof (serv_addr)) < 0)
   {
     perror ("无法连接");
     return 1;
   }
 ​
   message = "Hello there!";
   if (send (sock_fd, message, strlen (message), 0) < 0)
   {
     perror ("无法发送");
     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 ("无法读取");
   }
 ​
   return 0;
 }

这是相当标准的代码,并调用作为第一个参数给出的任何IP地址。如果你运行 nc -lp 5000 并在另一个终端窗口中运行 ./client 127.0.0.1,你应该会在netcat中看到消息出现,并且也能够将消息发送回 client

现在,我们可以开始玩一些有趣的东西了——正如我们上面看到的,我们可以注入字符串和指针到进程中。我们可以通过操作程序输出的 sockaddr_in 结构体来做同样的事情:

$ ./client 127.0.0.1
connect() 位于: 0x400780

这是serv_addr缓冲区:
02 00 13 88 7f 00 00 01 30 30 30 30 30 30 30 30
按ENTER键继续

如果你不完全熟悉结构体的结构,网上有很多资源可以告诉你其中的内容。这里重要的部分是字节 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("""
 // 首先,让我们给自己一些内存来存放我们的结构体:
 send('分配内存并写入字节...');
 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() 可以在不知道源模块的情况下找到函数,
 // 但它较慢,尤其是在大型二进制文件上!YMMV...
 Interceptor.attach(Module.getExportByName(null, 'connect'), {
     onEnter(args) {
         send('注入恶意字节数组:');
         args[1] = st;
     }
     //, onLeave(retval) {
     //   retval.replace(0); // 使用此方法操作返回值
     //}
 });
 """)
 ​
 # 这里是一些消息处理...
 # [ 作为输出阅读更有意义 :-D
 #   错误带有 [!] 前缀,消息带有 [i] 前缀。 ]
 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() 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


原文链接:Functions | Frida • A world-class dynamic instrumentation toolkit

翻译来源:DeepSeek


Comment