使用 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 引擎
开始跟踪之前执行。此功能的可能用途包括:
- 在第一个函数处理程序被调用之前,执行自定义代码以创建所需的代码对象和数据对象。
- 创建共享代码库,使经过精心调试的 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() {
}
};