How to use frida-trace's "init-session" option
This page describes uses for the frida-trace --init-session
/ -S
command
line option, and how to utilize it in your work.
What is the –init-session option?
The --init-session
option executes any number of user-written JavaScript code
files during the frida-trace engine initialization stage. These files are
executed before the first function handler is called.
Its power comes from the ability to define globally visible functions and store
data in the global state
object, which is passed as a parameter to every
handler called.
The state
object allows you to maintain information across function calls.
Data stored in state
are accessible to all called handlers.
Uses for the –init-session option
The --init-session
/ -S
option guarantees that JavaScript source code of
your choice is executed before the frida-trace engine begins its tracing.
Possible applications of this feature include:
- Executing custom code that creates both code and data objects of your choice, doing this before the first function handler is invoked.
- Creating a library of shared code, allowing the sharing of fine-tuned and debugged JavaScript code that can be called globally by any handler, at any time.
The frida-trace JavaScript code is often written as one-time “throw-away” code. If, however, you find yourself frequently copying and pasting code between handlers and projects, consider saving the code in a shared library. Once written and debugged, you can reuse the functions and data in future projects.
Detailed Example: Creating a shared-code library
In this example, we demonstrate using the --init-session
/ -S
option to
enhance tracing of the Microsoft Windows ExtTextOutW()
function. We describe
the components in a top-down fashion, beginning with the ExtTextOutW.js
JavaScript handler function, working our way down to the shared code files.
ExtTextOutW(): Function Signature
In my Windows system, the ExtTextOutW() function to monitor resides in gdi32full.dll. Here is the C syntax of the function:
BOOL ExtTextOutA(
HDC hdc,
int x,
int y,
UINT options,
const RECT *lprect,
LPCSTR lpString,
UINT c,
const INT *lpDx
);
Leveraging JavaScript code in our shared code libraries, we produce an enhanced tracing output.
The enhanced trace output
Before showing the handler code and the shared code libraries, here is the enhanced tracing output itself:
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
Notice the following tracing enhancements:
- The
options
field is converted to textual form - The
lprect
memory pointer is displayed as both a hex memory dump and a textual string - The
lpString
memory pointer is displayed as both a hex memory dump and a textual string - The
lpDx
integer pointer is displayed as both a hex memory dump and an integer
The textual conversions and hex memory dumps are functions within our shared code
libraries. Let’s see our ExtTextOutW()
handler JavaScript code, which will
make use of the shared code.
Handler: ExtTextOutW.js
Here is our ExeTextOutW() handler code. We enhance it by invoking shared code
functions. These functions exist in the outer scope, as any top-level functions
defined in session scripts become visible to all handlers. The functions are
defined when the shared code JavaScript files are executed by frida-trace via
the --init-session
/ -S
command line option.
/*
* 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()}`);
}
}
Note that, besides calling standard Frida functions (e.g., toInt32()
,
isNull()
, readUtf16String()
), there are calls to our shared code functions
(e.g., cloneArgs()
, decodeExttextoutOptions()
, prettyHexdump()
,
rectStructToString()
). The shared code functions have been debugged and
refined, and are ready to be called by any handler, at any time.
Shared Code: core.js
The “core.js” shared code library contains core, or basic, functions that are meant to be reused by frida-trace handlers and shared code libraries.
Writing a shared code library is simple: your shared library source files define
functions and data objects, storing the latter in the global state
object.
Once stored there, any handler can access them through 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})`);
}
}
Shared Code: ms-windows.js
The “ms-windows.js” shared code library consists of MS Windows-related utility
functions, and is built on top of the core.js
library.
/*
* 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})`;
}
The two -S
command line options provide the paths to the “core.js” and
“ms-windows.js” shared library source files.
Touching just about anything in the Word application with the cursor will generate ExtTextOutW() traces.
Points to Consider
Here are some points to consider when using the -S
option.
Use different code source files to group your shared functions
For clarity sake, you can have several shared code files for different groups of functions. In the above example, the common and basic functions reside in “core.js”, while MS Windows specific function are found in “ms-windows.js”. In other projects you may have files for Android-related functions, for Linux functions, etc.
Implementing namespaces
As the number of shared library code files you use grows, you may experience name clashes due to “namespace pollution”. This can occur if two different shared code files implement a function with the same name. If within your own organization, you can modify the name. If, however, you are using third-party shared code libraries, this might be more difficult.
A possible solution is for a shared library to store its functions on a global object named descriptively, and correspondingly for data stored on “state”.
Here is how you might implement it:
global.MyLibrary = {
doX() {
},
doY() {
}
};