Skip to content

Commit

Permalink
Added command args for filtering export names and improved quality of…
Browse files Browse the repository at this point in the history
… metadata output
  • Loading branch information
chame1eon committed Aug 14, 2019
1 parent 8042509 commit 24ca6c7
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 162 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# jnitrace Change Log

## 2.1.0
- Added two new command line arguments to filter the library exports from the trace
- Changed the way object data was associated with output to ensure it is still visible if the method is not being traced

## 2.0.1
- Fix a bug preventing frida attach from working
- Updated README
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ Optional arguments are listed below:
backtracer in `accurate` mode. This option can be changed to `fuzzy` mode or used to stop the backtrace
by using the `none` option. See the Frida docs for an explanation on the differences.
* `-i <regex>` - is used to specify the method names that should be traced. This can be helpful for reducing the noise in particularly large JNI apps. The option can be supplied multiple times. For example, `-i Get -i RegisterNatives` would include
only JNI methods that contain Get or RegisterNatives in their name. Warning, using these options may result in less rich output
information.
only JNI methods that contain Get or RegisterNatives in their name.
* `-e <regex>` - is used to specify the method names that should be ignored in the trace. This can be helpful for reducing the noise in particularly large JNI apps. The option can be supplied multiple times. For example, `-e ^Find -e GetEnv` would exclude from
the results all JNI method names that begin Find or contain GetEnv. Warning, using these options may result in less rich output
information.
the results all JNI method names that begin Find or contain GetEnv.
* `-I <string>` - is used to specify the exports from a library that should be traced. This is useful for libraries where you only
want to trace a small number of methods. The functions jnitrace considers exported are any functions that are directly callable
from the Java side, as such, that includes methods bound using RegisterNatives. The option can be supplied multiple times. For example,
`-I stringFromJNI -I nativeMethod([B)V` could be used to include an export from the library called `Java_com_nativetest_MainActivity_stringFromJNI` and a method bound using RegisterNames with the signature of `nativeMethod([B)V`.
* `-E <string>` is used to specify the exports from a library that should not be traced. This is useful for libraries where you
have a group of busy native calls that you want to ignore. The functions jnitrace considers exported are any functions that are directly callable from the Java side, as such, that includes methods bound using RegisterNatives. The option can be supplied multiple times. For example, `-E JNI_OnLoad -E nativeMethod` would exclude from the trace the `JNI_OnLoad` function call and any methods
with the name `nativeMethod`.
* `-o path/output.json` - is used to specify an output path where `jnitrace` will store all traced data. The information is stored in JSON format to allow later post-processing of the trace data.
* `-p path/to/script.js` - the path provided is used to load a Frida script into the target process before the `jnitrace` script has loaded. This can be used for defeating anti-frida or anti-debugging code before `jnitrace` starts.
* `-a path/to/script.js` - the path provided is used to load Frida script into the target process after `jnitrace` has been loaded.
Expand Down
95 changes: 35 additions & 60 deletions jnitrace/jnitrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,45 +70,23 @@ class TraceFormatter:
def __init__(self, _config, _buffer_output):
self._config = _config
self._buffer_output = _buffer_output
self._type_mappings = {
"jclasses": {},
"jmethods": {},
"jfields": {}
}

self._current_ts = None
self._color_manager = ColorManager()
self._output_buffer = []
self._is_64b = False

def _update_refs(self, method, args, ret):
if method["name"] in ["GetMethodID", "GetStaticMethodID"]:
method_id = ret
self._type_mappings["jmethods"][method_id] = {
"name": args[2]["data"],
"sig": args[3]["data"]
}
elif method["name"] in ["GetFieldID", "GetStaticFieldID"]:
field_id = ret
self._type_mappings["jfields"][field_id] = {
"name": args[2]["data"],
"sig": args[3]["data"]
}
elif method["name"] in ["FindClass", "DefineClass"]:
class_id = ret
self._type_mappings["jclasses"][class_id] = {
"name": args[1]["data"]
}

def _print_thread_id(self, thread_id):
print("{:15s}/* TID {:d} */{}".format(
print("{}{:15s}/* TID {:d} */{}".format(
Fore.WHITE,
self._color_manager.get_current_color(),
thread_id,
Style.RESET_ALL
))

def _print_method_name(self, struct_type, name):
print("{:7d} ms {}[+] {}->{}{}".format(
print("{}{:7d} ms {}[+] {}->{}{}".format(
Fore.WHITE,
self._current_ts,
self._color_manager.get_current_color(),
struct_type,
Expand All @@ -118,42 +96,23 @@ def _print_method_name(self, struct_type, name):

def _get_data_metadata(self, arg_type, value):
opt = None
if arg_type == "jfieldID":
val = value
jfields = self._type_mappings["jfields"]
if val in jfields:
opt = "{}:{}".format(jfields[val]["name"], jfields[val]["sig"])
else:
opt = "unknown"
elif arg_type == "jmethodID":
val = value
jmethods = self._type_mappings["jmethods"]
if val in jmethods:
opt = "{}{}".format(jmethods[val]["name"], jmethods[val]["sig"])
else:
opt = "unknown"
elif arg_type == "jclass":
val = value
jclasses = self._type_mappings["jclasses"]
if val in jclasses:
opt = jclasses[val]["name"]
else:
opt = "unknown"
elif arg_type == "jboolean":
if arg_type == "jboolean":
if value == 0:
opt = "false"
else:
opt = "true"
return opt

def _print_data_prefix(self):
print("{:7d} ms {}|".format(
print("{}{:7d} ms {}|".format(
Fore.WHITE,
self._current_ts,
self._color_manager.get_current_color()
), end="")

def _print_data_value(self, sym, value, arg_type=None, padding=0):
opt = self._get_data_metadata(arg_type, value)
def _print_data_value(self, sym, value, arg_type=None, opt=None, padding=0):
if not opt:
opt = self._get_data_metadata(arg_type, value)

self._print_data_prefix()

Expand All @@ -171,7 +130,7 @@ def _print_data_value(self, sym, value, arg_type=None, padding=0):
print(value, end="")

if opt:
print(" {{ {} }}".format(
print(" {{ {} }}".format(
opt
), end="")

Expand All @@ -181,7 +140,9 @@ def _print_data(self, block, arg_type, padding, data):
self._print_data_value(
block["sym"],
block["data"]["value"],
arg_type, padding
arg_type=arg_type,
opt=block["data"].get("metadata"),
padding=padding
)

if self._config["show_data"]:
Expand All @@ -194,8 +155,14 @@ def _print_arg_data(self, arg, arg_type=None, padding=0, data=None):
}
self._print_data(block, arg_type, padding, data)

def _print_arg_sub_data(self, arg, arg_type=None, padding=4):
self._print_data_value(":", arg, arg_type, padding)
def _print_arg_sub_data(self, arg, arg_type=None, opt=None, padding=4):
self._print_data_value(
":",
arg,
arg_type=arg_type,
opt=opt,
padding=padding
)

def _print_ret_data(self, ret, ret_type=None, padding=0, data=None):
block = {
Expand Down Expand Up @@ -240,6 +207,8 @@ def _print_args(self, method, args, java_params, data):
arg_type = method["args"][i]
if arg_type in ["...", "va_list", "jvalue*"]:
add_java_args = True

if arg_type == "...":
break

arg = args[i]
Expand All @@ -262,6 +231,7 @@ def _print_args(self, method, args, java_params, data):
self._print_arg_sub_data(
arg["value"],
arg_type=java_params[i],
opt=arg.get("metadata"),
padding=padding
)

Expand Down Expand Up @@ -359,6 +329,8 @@ def _update_output_buffer(self, payload, data):
output_arg["data"] = binascii.hexlify(data).decode()
elif "data" in arg:
output_arg["data"] = arg["data"]
if "metadata" in arg:
output_arg["metadata"] = arg["metadata"]
args.append(output_arg)

record["args"] = args
Expand All @@ -369,6 +341,8 @@ def _update_output_buffer(self, payload, data):

if "has_data" in payload["ret"]:
ret["data"] = binascii.hexlify(data).decode()
if "metadata" in payload["ret"]:
ret["metadata"] = payload["ret"]["metadata"]

record["ret"] = ret

Expand Down Expand Up @@ -402,11 +376,6 @@ def on_message(self, message, data):
self._update_output_buffer(message["payload"], data)

self._current_ts = payload["timestamp"]
method = payload["method"]
args = payload["args"]
ret = payload["ret"]

self._update_refs(method, args, ret.get("value"))

self._color_manager.update_current_color(payload["thread_id"])

Expand All @@ -433,6 +402,10 @@ def _parse_args():
help="A regex filter to include a JNIEnv or JavaVM method name.")
parser.add_argument("-e", "--exclude", action="append", default=[],
help="A regex filter to exclude a JNIEnv or JavaVM method name.")
parser.add_argument("-I", "--include-export", action="append", default=[],
help="A list of library exports to trace from.")
parser.add_argument("-E", "--exclude-export", action="append", default=[],
help="A list of library exports to avoid tracing from.")
parser.add_argument("--hide-data", action="store_true",
help="Print contents of argument.")
parser.add_argument("--ignore-env", action="store_true",
Expand Down Expand Up @@ -509,6 +482,8 @@ def main():
"show_data": not args.hide_data,
"include": args.include,
"exclude": args.exclude,
"include_export": args.include_export,
"exclude_export": args.exclude_export,
"env": not args.ignore_env,
"vm": not args.ignore_vm
}
Expand Down
55 changes: 48 additions & 7 deletions jnitrace/src/jni/jni_env_interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,51 @@ abstract class JNIEnvInterceptor {
const methods = args[METHOD_INDEX] as NativePointer;
const size = args[SIZE_INDEX] as number;
for (let i = 0; i < size * JNI_METHOD_SIZE; i += JNI_METHOD_SIZE) {
const methodsPtr = methods;

const namePtr = methodsPtr
.add(i * Process.pointerSize)
.readPointer();
const name = namePtr.readCString();

const sigOffset = 1;
const sigPtr = methodsPtr
.add((i + sigOffset) * Process.pointerSize)
.readPointer();
const sig = sigPtr.readCString();

const addrOffset = 2;
const offset = (i + addrOffset) * Process.pointerSize;
const addr = methods.add(offset).readPointer();
const addr = methodsPtr
.add((i + addrOffset) * Process.pointerSize)
.readPointer();

if (name === null || sig === null) {
continue;
}

Interceptor.attach(addr, {
onEnter(args): void {
const check = name + sig;
const config = Config.getInstance();
const EMPTY_ARRAY_LEN = 0;

if (config.includeExport.length > EMPTY_ARRAY_LEN) {
const included = config.includeExport.filter(
(i): boolean => check.includes(i)
);
if (included.length === EMPTY_ARRAY_LEN) {
return;
}
}
if (config.excludeExport.length > EMPTY_ARRAY_LEN) {
const excluded = config.excludeExport.filter(
(e): boolean => check.includes(e)
);
if (excluded.length > EMPTY_ARRAY_LEN) {
return;
}
}

if (!self.threads.hasJNIEnv(this.threadId)) {
self.threads.setJNIEnv(
this.threadId, args[JNI_ENV_INDEX]
Expand Down Expand Up @@ -287,12 +326,14 @@ abstract class JNIEnvInterceptor {

const ret = nativeFunction.apply(null, args);

let javaParams: string[] = [];
let jmethod: JavaMethod | undefined = undefined;
if (args.length !== clonedArgs.length) {
const key = args[METHOD_ID_INDEX].toString();
javaParams = self.methods[key].nativeParams;
jmethod = self.methods[key];
}
const data = new MethodData(method, clonedArgs, ret, javaParams);
const data = new MethodData(
method, clonedArgs, ret, jmethod
);

self.transport.reportJNIEnvCall(data, this.context);

Expand Down Expand Up @@ -323,15 +364,15 @@ abstract class JNIEnvInterceptor {
const args: NativeArgumentValue[] = [].slice.call(arguments);
const jniEnv = self.threads.getJNIEnv(threadId);
const key = args[METHOD_ID_INDEX].toString();
const nativeJTypeParams = self.methods[key].nativeParams;
const jmethod = self.methods[key];

args[JNI_ENV_INDEX] = jniEnv;

const ret = new NativeFunction(methodPtr,
retType,
initialparamTypes).apply(null, args);

const data = new MethodData(method, args, ret, nativeJTypeParams);
const data = new MethodData(method, args, ret, jmethod);

self.transport.reportJNIEnvCall(
data, self.vaArgsBacktraces[this.threadId]
Expand Down
25 changes: 23 additions & 2 deletions jnitrace/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ function checkLibrary(path: string): boolean {
message.payload.show_data,
message.payload.include,
message.payload.exclude,
message.payload.include_export,
message.payload.exclude_export,
message.payload.env,
message.payload.vm
);
Expand Down Expand Up @@ -167,13 +169,32 @@ if (dlopenRef !== null && dlsymRef !== null && dlcloseRef !== null) {
return;
}

this.symbolAddr = ptr(args[SYMBOL_INDEX].toString());
this.symbol = args[SYMBOL_INDEX].readCString();
},
onLeave(retval): void {
if (retval.isNull() || libBlacklist[this.handle]) {
return;
}

const EMPTY_ARRAY_LEN = 0;

if (config.includeExport.length > EMPTY_ARRAY_LEN) {
const included = config.includeExport.filter(
(i): boolean => this.symbol.includes(i)
);
if (included.length === EMPTY_ARRAY_LEN) {
return;
}
}
if (config.excludeExport.length > EMPTY_ARRAY_LEN) {
const excluded = config.excludeExport.filter(
(e): boolean => this.symbol.includes(e)
);
if (excluded.length > EMPTY_ARRAY_LEN) {
return;
}
}

if (trackedLibs[this.handle] === undefined) {
// Android 7 and above miss the initial dlopen call.
// Give it another chance in dlsym.
Expand All @@ -184,7 +205,7 @@ if (dlopenRef !== null && dlsymRef !== null && dlcloseRef !== null) {
}

if (trackedLibs[this.handle] !== undefined) {
const symbol = this.symbolAddr.readCString();
const symbol = this.symbol;
if (symbol === "JNI_OnLoad") {
interceptJNIOnLoad(ptr(retval.toString()));
} else if (symbol.startsWith("Java_") === true) {
Expand Down
Loading

0 comments on commit 24ca6c7

Please sign in to comment.