最佳实践

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

本节旨在介绍使用 Frida 时经常遇到的最佳实践和常见陷阱。

字符串分配(UTF-8/UTF-16/ANSI)

阅读文档后,你可能会认为分配/替换字符串就像下面这样简单:

onEnter(args) {
  args[0].writeUtf8String('mystring');
}

然而,这样做可能行不通,因为字符串所指向的位置可能:

  • 位于“只读数据”段中,该段会以只读方式映射到进程的地址空间;
  • 新字符串比原有字符串更长,导致 writeUtf8String() 发生缓冲区溢出, 并可能破坏无关的内存。

即使可以使用 Memory.protect() 解决前一个问题,也有一种好得多的办法: 分配一个新字符串,然后替换参数。

不过这里也有一个陷阱:必须让 Memory.allocUtf8String() 返回的值保持存活—— JavaScript 值一旦被垃圾回收,它就会被释放。这意味着至少在函数调用期间必须让它 保持存活;某些情况下甚至要更久,具体语义取决于 API 的设计方式。

考虑到这一点,可靠的写法如下:

onEnter(args) {
  const buf = Memory.allocUtf8String('mystring');
  this.buf = buf;
  args[0] = buf;
}

这里的工作原理是:this 会绑定到一个按线程、按调用分别创建的对象,存放在其中的 任何内容都可以在 onLeave 中使用,即使出现递归调用也同样有效。这样就可以在 onEnter 中读取参数,并在稍后的 onLeave 中访问它们。这也是在函数调用期间让 所分配内存保持存活的推荐方式。

如果函数会保留该指针,并在函数调用完成后继续使用它,一种解决办法是这样写:

const myStringBuf = Memory.allocUtf8String('mystring');

Interceptor.attach(f, {
  onEnter(args) {
    args[0] = myStringBuf;
  }
});

重复使用参数

在 onEnter 回调中读取参数时,通常会按索引访问各个参数。但如果同一个参数被访问 多次,会发生什么?以下面的代码为例:

Interceptor.attach(f, {
  onEnter(args) {
    if (!args[0].readUtf8String(4).includes('MZ')) {
      console.log(hexdump(args[0]));
    }
  }
});

在上面的示例中,第一个参数从 args 数组中取了两次,因此也付出了两次向 frida-gum 查询该信息的开销。需要多次使用同一个参数时,为避免浪费宝贵的 CPU 周期,最好用局部变量保存它:

Interceptor.attach(f, {
  onEnter(args) {
    const firstArg = args[0];
    if (!firstArg.readUtf8String(4).includes('MZ')) {
      console.log(hexdump(firstArg));
    }
  }
});