使用 frida-trace 的 init-session 选项

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

本页介绍 frida-trace 的 --init-session / -S 命令行选项有哪些用途, 以及如何在工作中使用它。

什么是 –init-session 选项?

--init-session 选项会在 frida-trace 引擎初始化阶段执行任意数量的用户编写的 JavaScript 代码文件。这些文件会在第一个函数处理程序被调用之前执行。

它的强大之处在于,可以定义全局可见的函数,并将数据存入全局 state 对象; 每个被调用的处理程序都会收到该对象作为参数。

state 对象让你能够跨函数调用维护信息。存储在 state 中的数据可供所有被调用的 处理程序访问。

–init-session 选项的用途

--init-session / -S 选项可保证你选择的 JavaScript 源代码在 frida-trace 引擎 开始跟踪之前执行。此功能的可能用途包括:

  1. 在第一个函数处理程序被调用之前,执行自定义代码以创建所需的代码对象和数据对象。
  2. 创建共享代码库,使经过精心调试的 JavaScript 代码可以随时由任何处理程序全局调用。

frida-trace JavaScript 代码经常作为一次性的“用后即弃”代码编写。不过,如果你发现自己 经常在处理程序和项目之间复制粘贴代码,可以考虑把代码保存到共享库中。完成编写和调试后, 便可在未来的项目中复用其中的函数和数据。

详细示例:创建共享代码库

本例演示如何使用 --init-session / -S 选项增强对 Microsoft Windows ExtTextOutW() 函数的跟踪。我们将采用自顶向下的方式介绍各个组成部分,先从 ExtTextOutW.js JavaScript 处理函数讲起,再逐步深入到共享代码文件。

ExtTextOutW():函数签名

在我的 Windows 系统中,要监控的 ExtTextOutW() 函数位于 gdi32full.dll 中。 以下是该函数的 C 语法:

BOOL ExtTextOutA(
  HDC        hdc,
  int        x,
  int        y,
  UINT       options,
  const RECT *lprect,
  LPCSTR     lpString,
  UINT       c,
  const INT  *lpDx
);

借助共享代码库中的 JavaScript 代码,我们可以获得增强后的跟踪输出。

增强后的跟踪输出

在展示处理程序代码和共享代码库之前,先来看增强后的跟踪输出:

c:\project> frida-trace -p 6980 --decorate -i "gdi32full.dll!ExtTextOutW" -S core.js -S ms-windows.js
Instrumenting...
ExtTextOutW: Loaded handler at "c:\\project\\__handlers__\\gdi32full.dll\\ExtTextOutW.js"
Started tracing 1 function. Press Ctrl+C to stop.
           /* TID 0x3ab8 */
  2695 ms  ---------------------------------------------
  2695 ms  ExtTextOutW() [gdi32full.dll]
  2695 ms  x: 0
  2695 ms  y: 0
  2695 ms  options: ETO_OPAQUE
  2695 ms  lprect [20 bytes]
                           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
                00b9c81c  00 00 00 00 01 00 00 00 01 00 00 00 98 02 00 00  ................
                00b9c82c  26 90 b2 b3                                      &...
  2695 ms  lprect: (left, top, right, bottom) = (0, 1, 1, 664)
  2695 ms  c: 0
  2696 ms  x (exit): 0
  2696 ms  y (exit): 0
  . . .
  2788 ms  ---------------------------------------------
  2788 ms  ExtTextOutW() [gdi32full.dll]
  2788 ms  x: 1
  2788 ms  y: 0
  2788 ms  options: ETO_CLIPPED | ETO_IGNORELANGUAGE
  2788 ms  lprect [20 bytes]
                      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
           00b9eaac  00 00 00 00 00 00 00 00 4d 00 00 00 0f 00 00 00  ........M.......
           00b9eabc  0a 78 7e c1                                      .x~.
  2788 ms  lprect: (left, top, right, bottom) = (0, 0, 77, 15)
  2788 ms  lpString [50 bytes]
                      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
           1aa90148  43 00 61 00 6c 00 69 00 62 00 72 00 69 00 20 00  C.a.l.i.b.r.i. .
           1aa90158  28 00 42 00 6f 00 64 00 79 00 29 00 29 00 75 00  (.B.o.d.y.).).u.
           1aa90168  20 00 77 00 61 00 6e 00 20 00 20 00 74 00 6f 00   .w.a.n. . .t.o.
           1aa90178  20 00                                             .
  2788 ms  *lpString: "Calibri (Body))u wan  to do"
  2788 ms  c: 14
  2788 ms  lpDx [4 bytes]
                      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
           09e71208  08 00 00 00                                      ....
  2788 ms  *lpDx: 8
  2789 ms  x (exit): 0
  2789 ms  y (exit): 0

请注意以下跟踪增强效果:

  • options 字段被转换为文本形式
  • lprect 内存指针同时以十六进制内存转储和文本字符串显示
  • lpString 内存指针同时以十六进制内存转储和文本字符串显示
  • lpDx 整数指针同时以十六进制内存转储和整数显示

文本转换和十六进制内存转储功能都来自共享代码库中的函数。下面来看一下会使用这些共享代码的 ExtTextOutW() 处理程序 JavaScript 代码。

处理程序:ExtTextOutW.js

以下是 ExeTextOutW() 处理程序代码。我们通过调用共享代码函数来增强它。这些函数存在于外层 作用域中,因为会话脚本里定义的所有顶层函数都会对所有处理程序可见。当 frida-trace 通过 --init-session / -S 命令行选项执行共享代码 JavaScript 文件时,这些函数就会被定义。

/*
 * Auto-generated by Frida. Please modify to match the signature of ExtTextOutW.
 * This stub is currently auto-generated from manpages when available.
 *
 * For full API reference, see: https://frida.re/docs/javascript-api/
 */

{
  onEnter(log, args, state) {
    /*
     * C syntax:
     *
     * BOOL ExtTextOutW(
     *   HDC        hdc,
     *   int        x,
     *   int        y,
     *   UINT       options,
     *   const RECT *lprect,
     *   LPCWSTR    lpString,
     *   UINT       c,
     *   const INT  *lpDx
     * );
    */

    log('---------------------------------------------');
    log('ExtTextOutW() [gdi32full.dll]');

    cloneArgs(args, 8, this);
    const [, x, y, options, lprect, lpString, c, lpDx] = this.args;

    log(`x: ${x.toInt32()}`);
    log(`y: ${y.toInt32()}`);

    log(`options: ${decodeExttextoutOptions(options)}`);

    if (!lprect.isNull()) {
      prettyHexdump(log, 'lprect', lprect, 20);
      log(`lprect: ${rectStructToString(lprect)}`);
    }

    if (!lpString.isNull()) {
      prettyHexdump(log, 'lpString', lpString, 50);
      log(`*lpString: "${lpString.readUtf16String()}"`);
    }

    log(`c: ${c.toUInt32()}`);

    if (!lpDx.isNull()) {
      prettyHexdump(log, 'lpDx', lpDx, 4);
      log(`*lpDx: ${lpDx.readU32()}`);
    }
  },

  onLeave(log, retval, state) {
    /*
     * We can access the onEnter arguments using `this.args`,
     * as cloneArgs() copied them there.
     */
    const [, x, y] = this.args;
    log(`x (exit): ${x.toInt32()}`);
    log(`y (exit): ${y.toInt32()}`);
  }
}

请注意,除了调用标准 Frida 函数(例如 toInt32()、isNull()、 readUtf16String())外,代码还调用了共享代码函数(例如 cloneArgs()、 decodeExttextoutOptions()、prettyHexdump()、rectStructToString())。 这些共享代码函数已经过调试和完善,可随时由任何处理程序调用。

共享代码:core.js

“core.js”共享代码库包含供 frida-trace 处理程序及其他共享代码库复用的核心或基础函数。

编写共享代码库很简单:共享库源文件负责定义函数和数据对象,并将后者存入全局 state 对象。 一旦存入其中,任何处理程序都可以通过 state.propertyName 访问这些对象。

/*
 * Collection of useful general-purpose Frida handler functions.
 */

/**
 * Creates a true JavaScript array as `invCtx.args`.  This array can be accessed
 * in the handler's onLeave().
 *
 * @param {NativePointer[]} args - The `args` array as passed to onEnter().
 * @param {number} numArgs - The number of meaningful arguments in `args`. This
 *     function has no way of determining the number of actual arguments because
 *     `args` is a virtual array.
 * @param {InvocationContext} invCtx - The `this` object of the calling onEnter().
 * @returns {NativePointer[]} Copy of `args`.
 */
function cloneArgs(args, numArgs, invCtx) {
  const items = [];
  for (let i = 0; i !== numArgs; i++)
    items.push(args[i]);
  invCtx.args = items;
}

/**
 * Returns a string describing the bitflags set in `value`.
 *
 * @param {number} value - A value consisting of zero or more bitflags.
 * @param {Map<number, string>} spec - A Map between:
 *     [hex value] -> [flag descriptive string]
 *     For example:
 *       new Map([
 *          [0x0004: 'ETO_CLIPPED'],
 *          [0x0010: 'ETO_GLYPH_INDEX'],
 *          ...
 *       ])
 * @returns {string} Flag names delimited by '|', or '0' if none are set.
 */
function decodeBitflags(value, spec) {
  if (value === 0)
    return '0';

  const flags = [];
  let pending = value;

  for (const [flagValue, flagName] of spec.entries()) {
    if ((value & flagValue) !== 0) {
      flags.push(flagName);
      pending &= ~flagValue;
      if (pending === 0)
        break;
    }
  }

  if (pending !== 0)
    flags.push(`0x${pending.toString(16)}`);

  return flags.join(' | ');
}

/**
 * Outputs the hex dump results to the log stream.  If you only want the dump
 * lines without outputing them to the log stream, use prettyHexdumpLines().
 *
 * @param {function} log - The log function to output to.
 * @param {string} desc - Descriptive text, printed together with the hex dump.
 * @param {NativePointer} address - Memory location to dump.
 * @param {number} length - Number of bytes to dump.
 */
function prettyHexdump(log, desc, address, length) {
  const lines = [];
  prettyHexdumpLines(lines, desc, address, length);
  log(lines.join('\n'));
}

/**
 * Produces a somewhat more elegant hex dump, based on Frida's own hexdump().
 * It does not output the hex dump to any stream, but rather returns the hex
 * dump lines in an array.  It is up to the caller to decide where to, and how,
 * to output the dump lines.
 *
 * @param {string[]} lines - Caller-provided array that will return the hex dump
 *     lines.
 * @param {string} desc - Descriptive text, printed together with the hex dump.
 * @param {NativePointer} address - Memory location to dump.
 * @param {number} length - Number of bytes to dump.
 * @param {string} [indent='\t\t'] - String to prepend to each dump line.
 */
function prettyHexdumpLines(lines, desc, address, length, indent = '\t\t') {
  lines.push(`${desc} [${length} bytes]`);

  try {
      const s = hexdump(address, { length });
      for (const line of s.split('\n')) {
        lines.push(`${indent}${line}`);
      }
  } catch (e) {
    lines.push(`${indent}WARNING: address is NOT VALID (${address})`);
  }
}

共享代码:ms-windows.js

“ms-windows.js”共享代码库由与 MS Windows 相关的实用函数组成,并构建在 core.js 库之上。

/*
 * Collection of useful Frida handler functions for MS Windows.
 */

const extTextOptionsSpec = new Map([
  [0x00004, 'ETO_CLIPPED'],
  [0x00010, 'ETO_GLYPH_INDEX'],
  [0x01000, 'ETO_IGNORELANGUAGE'],
  [0x00800, 'ETO_NUMERICSLATIN'],
  [0x00400, 'ETO_NUMERICSLOCAL'],
  [0x00002, 'ETO_OPAQUE'],
  [0x02000, 'ETO_PDY'],
  [0x00080, 'ETO_RTLREADING'],
  [0x10000, 'ETO_REVERSE_INDEX_MAP'],
]);

/**
 * Decodes ExtTextOutW() `options` bit flags.
 *
 * @param {number} flags - A DWORD consisting of the `options` bit flags used
 *     by ExtTextOutW().
 * @returns {string} Options delimited by '|', or '0' if none are set.
 */
function decodeExttextoutOptions(flags) {
  return decodeBitflags(flags, extTextOptionsSpec);
}

/**
 * Returns the four RECT values as a string of the form:
 *     (left, top, right, bottom) = (0, 0, 77, 15)
 *
 * @param {NativePointer} lprect - Pointer to a Windows RECT object.  The memory
 *     consists of four (4) contiguous LONG values, corresponding to the left,
 *     top, right, and bottom values, respectively.
 * @returns {string} Description of the RECT.
 */
function rectStructToString(lprect) {
  if (lprect.isNull()) {
    return 'LPRECT is null';
  }

  const left   = lprect.readU32();
  const top    = lprect.add(4).readU32();
  const right  = lprect.add(8).readU32();
  const bottom = lprect.add(12).readU32();

  return `(left, top, right, bottom) = (${left}, ${top}, ${right}, ${bottom})`;
}

两个 -S 命令行选项分别提供“core.js”和“ms-windows.js”共享库源文件的路径。

在 Word 应用程序中,用光标触碰几乎任何内容都会生成 ExtTextOutW() 跟踪记录。

注意事项

使用 -S 选项时,需要考虑以下几点。

使用不同的代码源文件对共享函数分组

为使结构清晰,可以为不同类别的函数准备多个共享代码文件。在上面的示例中,通用基础函数 位于“core.js”中,而 MS Windows 专用函数位于“ms-windows.js”中。在其他项目里, 还可以分别建立存放 Android 相关函数、Linux 函数等内容的文件。

实现命名空间

随着使用的共享库代码文件越来越多,你可能会因“命名空间污染”遇到名称冲突。如果两个不同的 共享代码文件实现了同名函数,就会发生这种情况。如果这些代码都属于你自己的组织,可以修改 名称;但如果使用第三方共享代码库,处理起来可能更困难。

一种可行的解决方案是,让共享库将其函数存放在一个命名清晰的全局对象上,并相应地将数据 存放到“state”上。

可以这样实现:

global.MyLibrary = {
  doX() {
  },
  doY() {
  }
};