frida官方文档部分翻译

读源码和看文档是一个好习惯。
官方文档

基本用法

枚举模块

enumerate_modules()方法列出了当前加载到目标进程会话中的所有模块(主要是共享/动态库)。运行print(s.enumerate_modules())返回结果应该像下面这样。

[Module(name="cat", base_address=0x400000, size=20480, path="/bin/cat"), ...]

base_address是模块的基地址。

枚举内存范围

enumerate_ranges(mask)方法列出了当前映射到目标进程会话的所有内存范围。运行print s.enumerate_ranges('rw-')返回结果应该像下面这样。

[Range(base_address=0x2d4160a06000, size=1019904, protection='rwx'), ...]

base_address是该范围的基址。该enumerate_ranges()方法需要来自rwx表中的保护掩码,而-可以视为通配符。

读/写内存

read_bytes(address, n)方法从目标进程的会话address处读取n个字节。write_bytes(address, data)方法将数据(原始Python字符串)中的字节写入address。运行print s.read_bytes(49758817247232, 10).encode("hex")应该返回给你一些454c4602010100000000这样的二进制数据;运行s.write_bytes(49758817247232, "frida")会在目标进程中更新内存后返回。

使用方式

Frida通过其功能强大的用C编写的插桩核心Gum提供动态插桩功能。由于这种插桩逻辑容易发生变化,您通常希望以脚本语言编写它,因此您可以在开发和维护时获得一个简短的反馈循环。这就是该GumJS发挥的地方。只需几行C代码,您可以在运行时运行一段JavaScript,完全访问Gum的API,允许您hook函数,枚举加载的库和它们导入和导出的函数,读取和写入内存,扫描内存等等。

注入

大多数情况下您想要生成一个现有的程序附加到正在运行的程序,或者在生成程序时劫持一个程序,然后在其中运行插桩逻辑。由于这是使用Frida的常见方法,所以我们的大多数文档都是关注的。该功能由frida-core提供,它作为一个物流层,将GumJS打包成一个共享库,将其注入到现有软件,如果需要的话提供一个双向通信通道用于与您的脚本通话,然后卸载它们。除了这个核心功能,frida-core还可以枚举已安装的应用程序,运行进程和连接设备。连接设备通常是运行frida-server的iOS和Android设备。该组件本质上只是一个通过TCP在localhost:27042暴露frida-core的守护进程。

嵌入

有时无法在注入模式中使用Frida,例如在被监视的iOS和Android系统上。对于这种情况,我们为您提供frida-gadget,它是一个共享库,您应该把它嵌入到您想插桩的应用程序中。一旦动态链接器执行其构造函数,该库就会开始运行,并显示与frida-server相同的接口 ,在localhost:27042上监听。唯一的区别是,运行的进程和安装的应用程序的列表只包含一个单独的入口,就是应用程序本身。进程名称始终只是Gadget,安装的应用程序的标识符始终是re.frida.Gadget。为了实现早期的插桩,我们让上述的构造函数阻塞,直到你将attach()附加到进程或者在通过通常的spawn()->attach()->…插桩…之后调用resume()。这意味着现有的CLI工具(如frida-trace)与您已经使用的方式相同。

预装

也许你熟悉LD_PRELOAD或DYLD_INSERT_LIBRARIES?如果有JS_PRELOAD,会不会很酷?这就是嵌入部分中讨论的共享库的frida-gadget还提供了第二种不涉及任何TCP或外部通信的操作模式。你只需要做的就是将FRIDA_GADGET_SCRIPT环境变量设置为包含JavaScript的文件的路径。例如在Linux上,只需创建包含以下内容hook.js的文件。

'use strict';

rpc.exports = {
  init: function () {
    Interceptor.attach(Module.findExportByName(null, 'open'), {
      onEnter: function (args) {
        var path = Memory.readUtf8String(args[0]);
        console.log('open("' + path + '")');
      }
    });
  }
};

您的操作系统的最新的frida-gadget可以在GitHub上找到。现在只需设置两个环境变量并启动您的目标进程。

LD_PRELOAD=/path/to/frida-gadget.so \ FRIDA_GADGET_SCRIPT=/path/to/hook.js \
cat /etc/hosts

在MacOS和iOS上使用DYLD_INSERT_LIBRARIES。请注意,/bin/cat将无法在El Capitan及以上版本上工作,因为它忽略了系统二进制程序的这种尝试。您还可以在自己开发插桩逻辑时添加FRIDA_GADGET_ENV=development,这将使frida-gadget观察您的文件的更改,并在磁盘上发生更改时自动重新加载脚本。如果您的脚本像上面这个例子中的那样hook函数,当旧版本的脚本被卸载时,所有的hook都会被自动恢复。我们暴露init()使用Frida RPC功能的方法的原因是因为frida-gadget会调用它,并等待它返回,直到程序继续执行其entrypoint为止。这意味着如果您需要进行异步操作,例如Memory.scan()来定位要插桩的函数,则可以返回Promise,并保证您不会错过任何早期通话。如果在进程退出之前需要执行一些明确的清理,或者从磁盘加载新版本 (FRIDA_GADGET_ENV=development),您还可以公开dispose()方法。您可以使用console.log(),console.warn()和 console.error()调试,它们将输出到stdout/stderr。

函数

我们将展示如何使用Frida在函数被调用、修改参数、对目标进程中的函数进行自定义调用时监视它们。

实验1-注入整数

创建一个文件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。

from __future__ import print_function
import frida
import sys

session = frida.attach("hello")
script = session.create_script(""" Interceptor.attach(ptr("%s"), { onEnter: function(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()

运行这个脚本。

$ python hook.py 0x400544

每秒应该返回一个新信息。

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

修改函数参数
接下来我们要修改传递给目标进程内的一个函数的参数。创建一个文件modify.py。

import frida
import sys

session = frida.attach("hello")
script = session.create_script(""" Interceptor.attach(ptr("%s"), { onEnter: function(args) { args[0] = ptr("1337"); } }); """ % int(sys.argv[1], 16))
script.load()
sys.stdin.read()

运行这个脚本。

$ python modify.py 0x400544

运行的终端hello process应该停止计数并始终返回1337,直到你使用Ctrl-D返回。

Number: 1281
Number: 1282
Number: 1337
Number: 1337
Number: 1337
Number: 1337
Number: 1296
Number: 1297
Number: 1298

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

import frida
import sys

session = frida.attach("hello")
script = session.create_script(""" var f = new NativeFunction(ptr("%s"), 'void', ['int']); f(1911); f(1911); f(1911); """ % int(sys.argv[1], 16))
script.load()

运行这个脚本。

$ python call.py 0x400544

观察终端的输出。

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

实验2-注入字符串

注入整数非常有用,但是我们也可以注入字符串,甚至你需要fuzz或者测试所需的任何其它类型的对象。创建一个文件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()。

from __future__ import print_function
import frida
import sys

session = frida.attach("hi")
script = session.create_script(""" var st = Memory.allocUtf8String("TESTMEPLZ!"); var 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语言中的结构体。这是一个创建网络套接字并在端口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 -l 5000并在另一个终端窗口运行 ./client 127.0.0.1,则应该会看到该消息出现在netcat中,并且还可以将消息发送回客户端。现在,事情变得有趣起来了-如上所述,我们可以在该过程中注入字符串和指针。我们可以通过操作程序运行过程中输出的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()函数以使得我们的新结构体作为参数。

from __future__ import print_function
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..."); var st = Memory.alloc(16); // Now we need to fill it - this is a bit blunt, but works... Memory.writeByteArray(st,[0x02, 0x00, 0x13, 0x89, 0x7F, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]); // Module.findExportByName() can find functions without knowing the source // module, but it's slower, especially over large binaries! YMMV... Interceptor.attach(Module.findExportByName(null, "connect"), { onEnter: function(args) { send("Injecting malicious byte array:"); args[1] = st; } //, onLeave: function(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.findExportByName()API在目标中按名称查找任何导出的函数。如果我们可以提供一个模块,那么在较大的二进制文件中查找会更快,但这在这里不太重要。现在,运行./client 127.0.0.1,在另一个终端运行中运行nc -l 5001,并在第三个终端运行./struct_mod.py。一旦我们的脚本运行,请在client终端窗口中按ENTER键,netcat现在应该显示客户端发送的字符串。我们已经成功地劫持了原始的网络,将自己的数据对象注入到内存中,用Frida hook我们的流程,并使用Interceptor来操纵函数实现恶意的行为。这显示了Frida的真正优势-没有patch,没有复杂的逆向过程,也没有花大量时间痛苦地盯着反汇编代码看而不知道什么时候能结束。
视频演示

消息

在本教程中,我们将展示如何向目标进程发送消息。

准备实验

创建一个文件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

从目标进程发送消息

以下脚本显示如何将消息发送回Python进程。你可以发送任何可序列化成JSON的JavaScript值。创建一个文件send.py。

from __future__ import print_function
import frida
import sys

session = frida.attach("hello")
script = session.create_script("send(1337);")
def on_message(message, data):
    print(message)
script.on('message', on_message)
script.load()
sys.stdin.read()

运行此脚本。

$ python send.py

它应该打印以下消息。

{u'type': u'send', u'payload': 1337}

这意味着JavaScript代码send(1337)已经在该hello进程中执行。终止脚本请按下Ctrl-D。

处理JavaScript运行时错误

如果JavaScript脚本引发一个未捕获的异常,那么它将从目标进程传到Python脚本中。如果将send(1337)用send(a)(未定义的变量)替换,Python中将收到以下消息。

{u'type': u'error', u'description': u'ReferenceError: a is not defined', u'lineNumber': 1}

注意type字段(error vs send)。

在目标进程中接收消息

可以将消息从Python脚本发送到JavaScript脚本。创建文件pingpong.py。

from __future__ import print_function
import frida
import sys

session = frida.attach("hello")
script = session.create_script(""" recv('poke', function onMessage(pokeMessage) { send('pokeBack'); }); """)
def on_message(message, data):
    print(message)
script.on('message', on_message)
script.load()
script.post({"type": "poke"})
sys.stdin.read()

运行脚本。

$ python pingpong.py

产生输出。

{u'type': u'send', u'payload': u'pokeBack'}

注:recv()的机制
recv()方法本身是异步的(非阻塞)。注册的回调(onMessage)将只收到一条消息。要接收下一条消息,必须使用recv()重新注册回调。

阻止目标进程收到消息

可以在JavaScript脚本中等待消息到达(阻止接收)。创建脚本rpc.py。

from __future__ import print_function
import frida
import sys

session = frida.attach("hello")
script = session.create_script(""" Interceptor.attach(ptr("%s"), { onEnter: function(args) { send(args[0].toString()); var op = recv('input', function(value) { args[0] = ptr(value.payload); }); op.wait(); } }); """ % int(sys.argv[1], 16))
def on_message(message, data):
    print(message)
    val = int(message['payload'], 16)
    script.post({'type': 'input', 'payload': str(val * 2)})
script.on('message', on_message)
script.load()
sys.stdin.read()

程序hello应该运行,你应该记下打印的地址(例如0x400544)。运行$ python rpc.py 0x400544然后观察终端运行中的hello的变化。

Number: 3
Number: 8
Number: 10
Number: 12
Number: 14
Number: 16
Number: 18
Number: 20
Number: 22
Number: 24
Number: 26
Number: 14

该hello程序应该输出偶数值,直到你停止运行Python脚本(Ctrl-D)。

Android

在本教程中,我们将介绍如何在Android设备上跟踪函数。

设置您的Android设备

在您开始之前,您将需要root您的设备。还要注意的是,我们大部分的测试都采用Android 4.4,目前我们正在支持4.2-6.0,现在对ART的支持有限,我们建议您暂时使用Dalvik驱动的ARM设备或模拟器。您还需要Android SDK中的adb工具。首先,在我们的releases page下载最新的frida-server,并在设备上运行。

$ adb root # might be required
$ adb push frida-server /data/local/tmp/ 
$ adb shell "chmod 755 /data/local/tmp/frida-server"
$ adb shell "/data/local/tmp/frida-server &"

最后一步请确保以root身份启动frida-server,即如果您在root过的设备上执行此操作,则可能需要su并从该shell运行它。接下来,确保adb中有您的设备。

$ adb devices -l

这也将确保adb守护进程在您的桌面上运行,这允许Frida发现您的设备并和它通信,无论您是通过USB连接还是WiFi连接。

快速冒烟测试(smoke-test)

现在,是时候确保基本功能正常工作了。运行$ frida-ps -U应该给你返回一个进程列表。-U代表USB,允许Frida检查USB设备,同时还可用于仿真器。

PID NAME
 1590 com.facebook.katana
13194 com.facebook.katana:providers
12326 com.facebook.orca
13282 com.twitter.android

太好了,我们继续吧!

在Chrome中跟踪open()调用

好的,让我们来找找乐子。启动手机上的Chrome app并返回到桌面,然后运行下面的命令。

$ frida-trace -U -i open com.android.chrome
Uploading data...
open: Auto-generated handler …/linker/open.js
open: Auto-generated handler …/libc.so/open.js
Started tracing 2 functions. Press Ctrl+C to stop.

现在打开Chrome app,您应该可以看到open()调用。

1392 ms open()
1403 ms open()
1420 ms open()

您现在可以即时编辑上述JavaScript文件 ,并开始更深入地了解Android app。

开发自己的工具

虽然像Frida,frida-trace等CLI工具肯定是非常有用的,但有时候您可能需要利用强大的Frida API开发自己的工具。为此,我们建议阅读函数和消息这两章,并且当你看到frida.attach()时把它们替换成frida.get_usb_device().attach()。

例子

windows

使用Frida监视fledge.exe进程(BB Simulator)执行的jvm.dll。将此代码保存为bb.py,运行BB Simulator(fledge.exe),然后运行 python bb.py fledge.exe以监视 jvm.dll的AES使用情况。

from __future__ import print_function
import frida
import sys

def on_message(message, data):
    print("[%s] => %s" % (message, data))

def main(target_process):
    session = frida.attach(target_process)

    script = session.create_script(""" // Find base address of current imported jvm.dll by main process fledge.exe var baseAddr = Module.findBaseAddress('Jvm.dll');
    console.log('Jvm.dll baseAddr: ' + baseAddr);

    var SetAesDeCrypt0 = resolveAddress('0x1FF44870'); // Here we use the function address as seen in our disassembler

    Interceptor.attach(SetAesDeCrypt0, { // Intercept calls to our SetAesDecrypt function

        // When function is called, print out its parameters
        onEnter: function (args) {
            console.log('');
            console.log('[+] Called SetAesDeCrypt0' + SetAesDeCrypt0);
            console.log('[+] Ctx: ' + args[0]);
            console.log('[+] Input: ' + args[1]); // Plaintext
            console.log('[+] Output: ' + args[2]); // This pointer will store the de/encrypted data
            console.log('[+] Len: ' + args[3]); // Length of data to en/decrypt
            dumpAddr('Input', args[1], args[3].toInt32());
            this.outptr = args[2]; // Store arg2 and arg3 in order to see when we leave the function
            this.outsize = args[3].toInt32();
        },

        // When function is finished
        onLeave: function (retval) {
            dumpAddr('Output', this.outptr, this.outsize); // Print out data array, which will contain de/encrypted data as output
            console.log('[+] Returned from SetAesDeCrypt0: ' + retval);
        }
    });

    function dumpAddr(info, addr, size) {
        if (addr.isNull())
            return;

        console.log('Data dump ' + info + ' :');
        var buf = Memory.readByteArray(addr, size);

        // If you want color magic, set ansi to true
        console.log(hexdump(buf, { offset: 0, length: size, header: true, ansi: false }));
    }

    function resolveAddress(addr) {
        var idaBase = ptr('0x1FEE0000'); // Enter the base address of jvm.dll as seen in your favorite disassembler (here IDA)
        var offset = ptr(addr).sub(idaBase); // Calculate offset in memory from base address in IDA database
        var result = baseAddr.add(offset); // Add current memory base address to offset of function to monitor
        console.log('[+] New addr=' + result); // Write location of function in memory to console
        return result;
    }
""") script.on('message', on_message)
    script.load()
    print("[!] Ctrl+D on UNIX, Ctrl+Z on Windows/cmd.exe to detach from instrumented program.\n\n")
    sys.stdin.read()
    session.detach()

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print "Usage: %s <process name or PID>" % __file__
        sys.exit(1)

    try:
        target_process = int(sys.argv[1])
    except ValueError:
        target_process = sys.argv[1]
    main(target_process)

Android

为Android CTF构建的示例工具

对于这个特定的例子,强烈建议使用Android 4.4 x86仿真器镜像。这个工具是基于SECCON Quals CTF 2015 APK1的例子,在这里下载APK。将代码另存为ctf.py并运行python ctf.py

import frida, sys

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

jscode = """ Java.perform(function () { // Function to hook is defined here var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity'); // Whenever button is clicked MainActivity.onClick.implementation = function (v) { // Show a message to know that the function got called send('onClick'); // Call the original onClick handler this.onClick(v); // Set our values after running the original onClick handler this.m.value = 0; this.n.value = 1; this.cnt.value = 999; // Log to the console that it's done, and we should have the flag! console.log('Done:' + JSON.stringify(this.cnt)); }; }); """

process = frida.get_usb_device().attach('com.example.seccon2015.rock_paper_scissors')
script = process.create_script(jscode)
script.on('message', on_message)
print('[*] Running CTF')
script.load()
sys.stdin.read()

工具

frida-ps

这是一个列出进行列表的命令行工具,在于远程系统交互的时候非常有用。

# Connect Frida to an iPad over USB and list running processes
$ frida-ps -U

# List running applications
$ frida-ps -Ua

# List installed applications
$ frida-ps -Uai

frida-trace

frida-trace是一个动态跟踪函数调用的工具。

# Trace recv* and send* APIs in Safari
$ frida-trace -i "recv*" -i "send*" Safari

# Trace ObjC method calls in Safari
$ frida-trace -m "-[NSView drawRect:]" Safari

# Launch SnapChat on your iPhone and trace crypto API calls
$ frida-trace -U -f com.toyopagroup.picaboo -I "libcommonCrypto*"

frida-discover

frida-discover是用于发现程序中内部函数的工具,然后可以使用frida-trace跟踪它。

相关文章
相关标签/搜索