From fce5c5c606f072c2dc897b5ec75a57ec8ab5ab37 Mon Sep 17 00:00:00 2001 From: iddoeldor Date: Fri, 8 Jun 2018 16:57:08 +0300 Subject: [PATCH] scripts directory #2 --- scripts/check_for_native_calls.py | 83 +++++++++ scripts/dump_dynamically_created_files.py | 119 +++++++++++++ scripts/exec_shell_cmd.py | 75 ++++++++ .../how_to_access_inner_class_static_field.md | 73 ++++++++ scripts/install_frida_server.sh | 20 +++ .../log_string_builders_and_string_compare.js | 23 +++ scripts/objc_ssl_unppining_helper.js | 105 +++++++++++ scripts/print_native_method_arguments.py | 164 ++++++++++++++++++ 8 files changed, 662 insertions(+) create mode 100644 scripts/check_for_native_calls.py create mode 100644 scripts/dump_dynamically_created_files.py create mode 100644 scripts/exec_shell_cmd.py create mode 100644 scripts/how_to_access_inner_class_static_field.md create mode 100644 scripts/install_frida_server.sh create mode 100644 scripts/log_string_builders_and_string_compare.js create mode 100644 scripts/objc_ssl_unppining_helper.js create mode 100644 scripts/print_native_method_arguments.py diff --git a/scripts/check_for_native_calls.py b/scripts/check_for_native_calls.py new file mode 100644 index 0000000..61f3dfd --- /dev/null +++ b/scripts/check_for_native_calls.py @@ -0,0 +1,83 @@ +# Check for native library calls and return a stacktrace +import sys +import frida +from pprint import pprint + + +def on_message(m, _data): + if m['type'] == 'send': + print(m['payload']) + else: + if m['type'] == 'error': + pprint(m) + exit(2) + + +jscode = """ +Java.perform(function() { + + var SystemDef = Java.use('java.lang.System'); + + var RuntimeDef = Java.use('java.lang.Runtime'); + + var exceptionClass = Java.use('java.lang.Exception'); + + var SystemLoad_1 = SystemDef.load.overload('java.lang.String'); + + var SystemLoad_2 = SystemDef.loadLibrary.overload('java.lang.String'); + + var RuntimeLoad_1 = RuntimeDef.load.overload('java.lang.String'); + + var RuntimeLoad_2 = RuntimeDef.loadLibrary.overload('java.lang.String'); + + var ThreadDef = Java.use('java.lang.Thread'); + + var ThreadObj = ThreadDef.$new(); + + SystemLoad_1.implementation = function(library) { + send("[1] Loading dynamic library => " + library); + stackTrace(); + return SystemLoad_1.call(this, library); + } + + SystemLoad_2.implementation = function(library) { + send("[2] Loading dynamic library => " + library); + stackTrace(); + SystemLoad_2.call(this, library); + return; + } + + RuntimeLoad_1.implementation = function(library) { + send("[3] Loading dynamic library => " + library); + stackTrace(); + RuntimeLoad_1.call(this, library); + return; + } + + RuntimeLoad_2.implementation = function(library) { + send("[4] Loading dynamic library => " + library); + stackTrace(); + RuntimeLoad_2.call(this, library); + return; + } + + function stackTrace() { + var stack = ThreadObj.currentThread().getStackTrace(); + for (var i = 0; i < stack.length; i++) { + send(i + " => " + stack[i].toString()); + } + send("--------------------------------------------------------------------------"); + } + +}); +""" +APP = 'com.app' +device = frida.get_usb_device() +pid = device.spawn([APP]) +session = device.attach(pid) +script = session.create_script(jscode) +print("[*] Intercepting [{}]".format(pid)) +script.on('message', on_message) +script.load() +device.resume(APP) +sys.stdin.read() diff --git a/scripts/dump_dynamically_created_files.py b/scripts/dump_dynamically_created_files.py new file mode 100644 index 0000000..a4c1ded --- /dev/null +++ b/scripts/dump_dynamically_created_files.py @@ -0,0 +1,119 @@ +import os +import sys +import frida + +APP_NAME = 'com.package.name' + + +def on_message(message, _ignored_data): + if message['type'] == 'send': + if type(message['payload']) == dict: + os.makedirs(os.path.dirname('./dump/{}/'.format(APP_NAME)), exist_ok=True) # create sub folder if not exist + with open('./dump/{}/{}'.format(APP_NAME, message['payload']['file']), 'w') as d: + for element in message['payload']['content']: + d.write(chr(element % 256)) + d.close() + print('[*] Successfully dumped to {0}'.format(message['payload']['file'])) + else: + print('[*] {0}'.format(message['payload'].encode('utf-8'))) + else: + print(message) + + +js_code = """ + Java.perform(function() { + var openedfile = ""; + var data = { + "file": "", + "content": [] + }; + var isOpen = false; + var index = 0; + + var fos = Java.use('java.io.FileOutputStream'); + + var fos_construct_2 = fos.$init.overload('java.lang.String'); + var fos_construct_3 = fos.$init.overload('java.io.File'); + var fos_construct_4 = fos.$init.overload('java.lang.String', 'boolean'); + var fos_construct_5 = fos.$init.overload('java.io.File', 'boolean'); + + var fos_write_1 = fos.write.overload('[B', 'int', 'int'); + + var fos_close = fos.close; + + function dump(data) { + send("Got " + data["content"].length + " bytes!"); + var tmp_name = openedfile.split("/"); + tmp_name = tmp_name[tmp_name.length - 1]; + data["file"] = tmp_name; + send(data); + data["content"] = []; + index = 0; + } + + fos_construct_2.implementation = function(file) { + var filename = file; + if (openedfile != filename) { + openedfile = filename; + send("File opened for write " + filename); + isOpen = true; + } + return fos_construct_2.call(this, file); + } + + fos_construct_3.implementation = function(file) { + var filename = file.getAbsolutePath(); + if (openedfile != filename) { + openedfile = filename; + send("File opened for write " + filename); + isOpen = true; + } + return fos_construct_3.call(this, file); + } + + fos_construct_4.implementation = function(file, true_false) { + var filename = file; + if (openedfile != filename) { + openedfile = filename; + send("File opened for write " + filename); + isOpen = true; + } + return fos_construct_4.call(this, file, true_false); + } + + fos_construct_5.implementation = function(file, true_false) { + var filename = file.getAbsolutePath(); + if (openedfile != filename) { + openedfile = filename; + send("File opened for write " + filename); + isOpen = true; + } + return fos_construct_5.call(this, file, true_false); + } + + fos_write_1.implementation = function(arr, offset, length) { + var i = 0; + for (i = offset; i < length; i = i + 1) { + data["content"][index] = arr[i]; + index = index + 1; + } + return fos_write_1.call(this, arr, offset, length); + } + + fos_close.implementation = function() { + dump(data); + return fos_close.call(this); + } + + }); +""" + +device = frida.get_usb_device() +pid = device.spawn([APP_NAME]) +session = device.attach(pid) +script = session.create_script(js_code) +print("[*] Intercepting [{}]".format(pid)) +script.on('message', on_message) +script.load() +device.resume(APP_NAME) +sys.stdin.read() diff --git a/scripts/exec_shell_cmd.py b/scripts/exec_shell_cmd.py new file mode 100644 index 0000000..6fc1e31 --- /dev/null +++ b/scripts/exec_shell_cmd.py @@ -0,0 +1,75 @@ +""" +Execute shell command +For example, list directory contents: +def ls(folder): + cmd = Shell(['/bin/sh', '-c', 'ls -la ' + folder], None) + cmd.exec() + for chunk in cmd.output: + print(chunk.strip().decode()) +""" +import frida +from frida.application import Reactor +import threading +import click + + +class Shell(object): + def __init__(self, argv, env): + self._stop_requested = threading.Event() + self._reactor = Reactor(run_until_return=lambda reactor: self._stop_requested.wait()) + + self._device = frida.get_usb_device() + self._sessions = set() + + self._device.on("child-added", lambda child: self._reactor.schedule(lambda: self._on_child_added(child))) + self._device.on("child-removed", lambda child: self._reactor.schedule(lambda: self._on_child_removed(child))) + self._device.on("output", lambda pid, fd, data: self._reactor.schedule(lambda: self._on_output(pid, fd, data))) + + self.argv = argv + self.env = env + self.output = [] # stdout will pushed into array + + def exec(self): + self._reactor.schedule(lambda: self._start()) + self._reactor.run() + + def _start(self): + click.secho("✔ spawn(argv={})".format(self.argv), fg='green', dim=True) + pid = self._device.spawn(self.argv, env=self.env, stdio='pipe') + self._instrument(pid) + + def _stop_if_idle(self): + if len(self._sessions) == 0: + self._stop_requested.set() + + def _instrument(self, pid): + click.secho("✔ attach(pid={})".format(pid), fg='green', dim=True) + session = self._device.attach(pid) + session.on("detached", lambda reason: self._reactor.schedule(lambda: self._on_detached(pid, session, reason))) + click.secho("✔ enable_child_gating()", fg='green', dim=True) + session.enable_child_gating() + # print("✔ resume(pid={})".format(pid)) + self._device.resume(pid) + self._sessions.add(session) + + def _on_child_added(self, child): + click.secho("⚡ child_added: {}".format(child), fg='green', dim=True) + self._instrument(child.pid) + + @staticmethod + def _on_child_removed(child): + click.secho("⚡ child_removed: {}".format(child), fg='green', dim=True) + + def _on_output(self, pid, fd, data): + # if len(data) > 0: + # print("⚡ output: pid={}, fd={}, data={}".format(pid, fd, repr(data))) + self.output.append(data) + + def _on_detached(self, pid, session, reason): + click.secho("⚡ detached: pid={}, reason='{}'".format(pid, reason), fg='green', dim=True) + self._sessions.remove(session) + self._reactor.schedule(self._stop_if_idle, delay=0.5) + + @staticmethod + def _on_message(pid, message): + click.secho("⚡ message: pid={}, payload={}".format(pid, message), fg='green', dim=True) diff --git a/scripts/how_to_access_inner_class_static_field.md b/scripts/how_to_access_inner_class_static_field.md new file mode 100644 index 0000000..c63e702 --- /dev/null +++ b/scripts/how_to_access_inner_class_static_field.md @@ -0,0 +1,73 @@ +### How to access inner class static field +``` +package tech.yusi.fridademo; + +public class Jingdong { + private int intResult; + + private final static class a { + final static Jingdong a = new Jingdong(); + } + + + public Jingdong() { + intResult = 0; + } + + public static Jingdong a() { + return a.a; + } + + public static int a(int arg0, int arg1) { + return arg0 + arg1; + } + + + public String a(String arg0, String arg1) { + return arg0 + arg1; + } +} +``` + +``` +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import frida,sys + +rdev = frida.get_remote_device() +session = rdev.attach("tech.yusi.fridademo") + +def on_message(message ,data): + if message['type'] == 'send': + print(message['payload']) + elif message['type'] == 'error': + print(message['stack']) + else: + print(message) + +jscode = """ +send(Java.available); +Java.perform(function () { + var JingdongA = Java.use("tech.yusi.fridademo.Jingdong$a"); + var Jingdong = JingdongA.a; + send(Jingdong.fieldType); + + var JingdongInstance = Jingdong.value; + var ret = JingdongInstance.a("G8", "4tar"); + send(ret); + +}); +""" + +script = session.create_script(jscode) +script.on("message" , on_message) +script.load() + +try: + sys.stdin.read() +except KeyboardInterrupt as e: + session.detach() + sys.exit(0) +``` + diff --git a/scripts/install_frida_server.sh b/scripts/install_frida_server.sh new file mode 100644 index 0000000..dfa506a --- /dev/null +++ b/scripts/install_frida_server.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Download latest frida-server, extract, push & run on android device/emulator +# adb 1.0.32, jq 1.5, xz 5.1, wget 1.17.1 +# sudo apt install wget jq xz + +# PARCH = phone architecture +# if oneliner [[ == "armeabi-v7a" ]] is a dirty fix because frida's release for armeabi-v7a is just "arm" + +# TODO fix adb root which does not work on phones, only emulators, use `adb shell su` instead + +PARCH=`adb shell getprop ro.product.cpu.abi`;\ +[[ "${PARCH}" == "armeabi-v7a" ]] && PARCH="arm";\ +wget -q -O - https://api.github.com/repos/frida/frida/releases \ +| jq '.[0] | .assets[] | select(.browser_download_url | match("server(.*?)android-'${PARCH}'*\\.xz")).browser_download_url' \ +| xargs wget -q --show-progress $1 \ +&& unxz frida-server* \ +&& adb root \ +&& adb push frida-server* /data/local/tmp/frida-server \ +&& adb shell "chmod 755 /data/local/tmp/frida-server" \ +&& adb shell "/data/local/tmp/frida-server &" diff --git a/scripts/log_string_builders_and_string_compare.js b/scripts/log_string_builders_and_string_compare.js new file mode 100644 index 0000000..18079db --- /dev/null +++ b/scripts/log_string_builders_and_string_compare.js @@ -0,0 +1,23 @@ +Java.perform(function() { + // string compare + var str = Java.use('java.lang.String'), objectClass = 'java.lang.Object'; + str.equals.overload(objectClass).implementation = function(obj) { + var response = str.equals.overload(objectClass).call(this, obj); + if (obj) { + if (obj.toString().length > 5) { + send(str.toString.call(this) + ' == ' + obj.toString() + ' ? ' + response); + } + } + return response; + } + // log AbstractStringBuilder.toString() + ['java.lang.StringBuilder', 'java.lang.StringBuffer'].forEach(function(clazz, i) { + console.log('[?] ' + i + ' = ' + clazz); + var func = 'toString'; + Java.use(clazz)[func].implementation = function() { + var ret = this[func](); + send('[' + i + '] ' + ret); + return ret; + }; + }); +}); diff --git a/scripts/objc_ssl_unppining_helper.js b/scripts/objc_ssl_unppining_helper.js new file mode 100644 index 0000000..0f2641c --- /dev/null +++ b/scripts/objc_ssl_unppining_helper.js @@ -0,0 +1,105 @@ +/* +* By http://github.com/LotemBY * + +This is a frida script for unpinning and reversing of ObjC applications. +Intercept method's which match regex. + +You may change the following regex arrays to match your needs: +*/ + +// The list of regexs for the moudle name +var moduleKeyWords = [/.*/]; // (It is not recommended to search all the moudles) + +// The list of regexs for the method name +var methodKeyWords = [/cert/i, /trust/i, /ssl/i, /verify/i, /509/]; + +// The list of regexs for the method to override their return value with "1" +var overrideKeyWords = []; + +/* +To run this script with frida on iPhone, follow these steps: + 1. Make sure the iPhone is jailbreaked + 2. Download the frida server from Cydia (package: re.frida.server) + 3. Connect the iPhone to your computer with USB and open the application + 4. Type in console "frida-ps -U" to get the list of running proccess on the iPhone, and find the proccess name of your app + 5. Type in console "frida -U -l " to run this script + 6. Now you should use the app to trigger some of the intercepted methods +*/ +var onCompleteCallback = function (retval) {}; +setImmediate(function () { + if (!ObjC.available) { + console.log("[-] Objective-C Runtime is not available!"); + return; + } + + console.log("=======================================================\n"); + console.log("[*] Searching methods..."); + + var moduleUsed = false; + + Process.enumerateModules({ + onMatch: function(module) { + + if (!matchesRegex(moduleKeyWords, module.name)) { + return; + } + + moduleUsed = false; + Module.enumerateSymbols(module.name, { + onMatch: function(exp) { + if (matchesRegex(methodKeyWords, exp.name)) { + if (!moduleUsed) { + console.log("[*] In module \"" + module.name + "\""); + moduleUsed = true; + } + console.log("\t[*] Matching method: \"" + exp.name + "\", Address: " + Module.findExportByName(module.name, exp.name)); + + if (intercept(module.name, exp.name)) { + console.log("\t\t[+] Now intercepting " + exp.name); + } else { + console.log("\t\t[-] Could not intercept " + exp.name); + } + } + }, + onComplete: onCompleteCallback + }); + }, + onComplete: onCompleteCallback + }); + + console.log("[*] Completed!"); + console.log("=======================================================\n\n"); +}); + +// Return if 'str' match any of the regexs in the array 'regexList' +function matchesRegex(regexList, str) { + regexList.forEach(function(el) { + if (str.search(el) != -1) + return true; + }); + return false; +} + +// Try to intercept a method by moudle name and function name. +// Return 'true' on success and 'false' on failor. +function intercept(module, func) { + try { + Interceptor.attach(Module.findExportByName(module, func), { + onEnter: function(args) { + console.log("[*] Method CALL:\t\"" + func + "\" called!"); + }, + onLeave: function (retval) { + console.log("[*] Method RETURN:\t\"" + func + "\" (return value: " + retval + ")"); + + if (matchesRegex(overrideKeyWords, func)) { + console.log("[!] CHANGED RETURN VALUE of method:\t\"" + func + "\" (new value: " + 1 + ")"); + retval.replace(1); + } + } + }); + + return true; + } catch (err) { + return false; + } +} diff --git a/scripts/print_native_method_arguments.py b/scripts/print_native_method_arguments.py new file mode 100644 index 0000000..363254a --- /dev/null +++ b/scripts/print_native_method_arguments.py @@ -0,0 +1,164 @@ +def on_message(m, _data): + if m['type'] == 'send': + print(m['payload']) + elif m['type'] == 'error': + print(m) + + +def switch(argument_key, idx): + """ + c/c++ variable type to javascript reader switch implementation + # TODO handle other arguments, [long, longlong..] + :param argument_key: variable type + :param idx: index in symbols array + :return: javascript to read the type of variable + """ + argument_key = argument_key.replace(' ', '') + return '%d: %s' % (idx, { + 'int': 'args[%d].toInt32(),', + 'unsignedint': 'args[%d].toInt32(),', + 'std::string': 'Memory.readUtf8String(Memory.readPointer(args[%d])),', + 'bool': 'Boolean(args[%d]),' + }[argument_key] % idx) + + +def list_symbols_from_object_files(module_id): + import subprocess + return subprocess.getoutput('nm --demangle --dynamic %s' % module_id) + + +def parse_nm_output(nm_stdout, symbols): + for line in nm_stdout.splitlines(): + split = line.split() + open_parenthesis_idx = line.find('(') + raw_arguments = [] if open_parenthesis_idx == -1 else line[open_parenthesis_idx + 1:-1] + if len(raw_arguments) > 0: # ignore methods without arguments + raw_argument_list = raw_arguments.split(',') + symbols.append({ + 'address': split[0], + 'type': split[1], # @see Symbol Type Table + 'name': split[2][:split[2].find('(')], # method name + 'args': raw_argument_list + }) + + +def get_js_script(method, module_id): + js_script = """ + var moduleName = "{{moduleName}}", nativeFuncAddr = {{methodAddress}}; + Interceptor.attach(Module.findExportByName(null, "dlopen"), { + onEnter: function(args) { + this.lib = Memory.readUtf8String(args[0]); + console.log("[*] dlopen called with: " + this.lib); + }, + onLeave: function(retval) { + if (this.lib.endsWith(moduleName)) { + Interceptor.attach(Module.findBaseAddress(moduleName).add(nativeFuncAddr), { + onEnter: function(args) { + console.log("[*] hook invoked", JSON.stringify({{arguments}}, null, '\t')); + } + }); + } + } + }); + """ + replace_map = { + '{{moduleName}}': module_id, + '{{methodAddress}}': '0x' + method['address'], + '{{arguments}}': '{' + ''.join([switch(method['args'][i], i + 1) for i in range(len(method['args']))]) + '}' + } + for k, v in replace_map.items(): + js_script = js_script.replace(k, v) + print('[+] JS Script:\n', js_script) + return js_script + + +def main(app_id, module_id, method): + """ + $ python3.x+ script.py --method SomeClass::someMethod --app com.company.app --module libfoo.so + :param app_id: application identifier / bundle id + :param module_id: shared object identifier / known suffix, will iterate loaded modules (@see dlopen) + :param method: method/symbol name + :return: hook native method and print arguments when invoked + """ + # TODO extract all app's modules via `adb shell -c 'ls -lR /data/app/' + app_if + '*' | grep "\.so"` + + nm_stdout = list_symbols_from_object_files(module_id) + + symbols = [] + parse_nm_output(nm_stdout, symbols) + + selection_idx = None + for idx, symbol in enumerate(symbols): + if method is None: # if --method flag is not passed + print("%4d) %s (%d)" % (idx, symbol['name'], len(symbol['args']))) + elif method == symbol['name']: + selection_idx = idx + break + if selection_idx is None: + if method is None: + selection_idx = input("Enter symbol number: ") + else: + print('[+] Method not found, remove method flag to get list of methods to select from, `nm` stdout:') + print(nm_stdout) + exit(2) + + method = symbols[int(selection_idx)] + print('[+] Selected method: %s' % method['name']) + print('[+] Method arguments: %s' % method['args']) + + from frida import get_usb_device + device = get_usb_device() + pid = device.spawn([app_id]) + session = device.attach(pid) + script = session.create_script(get_js_script(method, module_id)) + script.on('message', on_message) + script.load() + device.resume(app_id) + # keep hook alive + from sys import stdin + stdin.read() + + +if __name__ == '__main__': + from argparse import ArgumentParser + parser = ArgumentParser() + parser.add_argument('--app', help='app identifier "com.company.app"') + parser.add_argument('--module', help='loaded module name "libfoo.2.so"') + parser.add_argument('--method', help='method name "SomeClass::someMethod", if empty it will print select-list') + args = parser.parse_args() + main(args.app, args.module, args.method) + + +""" +Symbol Type Table: + "A" The symbol's value is absolute, and will not be changed by further linking. + "B" The symbol is in the uninitialized data section (known as BSS). + "C" The symbol is common. Common symbols are uninitialized data. + When linking, multiple common symbols may appear with the same name. + If the symbol is defined anywhere, the common symbols are treated as undefined references. + "D" The symbol is in the initialized data section. + "G" The symbol is in an initialized data section for small objects. + Some object file formats permit more efficient access to small data objects, such as a global int variable as + opposed to a large global array. + "I" The symbol is an indirect reference to another symbol. + This is a GNU extension to the a.out object file format which is rarely used. + "N" The symbol is a debugging symbol. + "R" The symbol is in a read only data section. + "S" The symbol is in an uninitialized data section for small objects. + "T" The symbol is in the text (code) section. + "U" The symbol is undefined. + "V" The symbol is a weak object. When a weak defined symbol is linked with a normal defined symbol, + the normal defined symbol is used with no error. + When a weak undefined symbol is linked and the symbol is not defined, the value of the weak symbol becomes + zero with no error. + "W" The symbol is a weak symbol that has not been specifically tagged as a weak object symbol. + When a weak defined symbol is linked with a normal defined symbol, + the normal defined symbol is used with no error. + When a weak undefined symbol is linked and the symbol is not defined, the value of the symbol is determined + in a system-specific manner without error. + On some systems, uppercase indicates that a default value has been specified. + "-" The symbol is a stabs symbol in an a.out object file. + In this case, the next values printed are the stabs other field, the stabs desc field, and the stab type. + Stabs symbols are used to hold debugging information. + "?" The symbol type is unknown, or object file format specific. +"""