├── .github └── ISSUE_TEMPLATE │ ├── bug-report.yaml │ ├── bug_report.md │ └── feature_request.yaml ├── .gitignore ├── README.md ├── agent.js ├── index.js ├── modules └── agentHandler.js └── package.json /.github/ISSUE_TEMPLATE/bug-report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: dropdown 10 | id: packages 11 | attributes: 12 | label: Which packages are you using? 13 | description: You may select more than one. 14 | multiple: true 15 | options: 16 | - stream_chat 17 | - stream_chat_flutter 18 | - stream_chat_flutter_core 19 | - stream_chat_persistance 20 | - stream_chat_localizations 21 | validations: 22 | required: true 23 | - type: dropdown 24 | id: platforms 25 | attributes: 26 | label: On what platforms did you experience the issue? 27 | description: You may select more than one. 28 | multiple: true 29 | options: 30 | - iOS 31 | - Android 32 | - Web 33 | - Windows 34 | - MacOS 35 | - Linux 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: version 40 | attributes: 41 | label: What version are you using? 42 | description: Please specify the package names and versions 43 | placeholder: package - version 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: what-happened 48 | attributes: 49 | label: What happened? 50 | description: Also, what did you expect to happen? 51 | placeholder: Description of the bug and what was expected. 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: repro-steps 56 | attributes: 57 | label: Steps to reproduce 58 | description: How do you trigger this bug? Please walk us through it step by step. 59 | value: | 60 | 1. Go to '...' 61 | 2. Click on '...' 62 | 3. Scroll down to '...' 63 | ... 64 | render: bash 65 | validations: 66 | required: true 67 | - type: textarea 68 | id: reproduce 69 | attributes: 70 | label: Supporting info to reproduce 71 | description: Please add any relevant code, screenshots and info needed to reproduce this issue. 72 | - type: textarea 73 | id: logs 74 | attributes: 75 | label: Relevant log output 76 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 77 | render: shell 78 | - type: textarea 79 | id: flutter-analyze 80 | attributes: 81 | label: Flutter analyze output 82 | description: Paste the output of `flutter analyze` here. 83 | placeholder: If there are any analysis errors, try resolving them before filing this issue. 84 | render: shell 85 | - type: textarea 86 | id: flutter-doctor 87 | attributes: 88 | label: Flutter doctor output 89 | description: Paste the output of `flutter doctor -v` here. 90 | render: shell 91 | - type: checkboxes 92 | id: terms 93 | attributes: 94 | label: Code of Conduct 95 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GetStream/stream-chat-flutter/blob/develop/CODE_OF_CONDUCT.md) 96 | options: 97 | - label: "I agree to follow this project's Code of Conduct" 98 | required: true 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to help us improve! 9 | - type: dropdown 10 | id: packages 11 | attributes: 12 | label: Please select which package this feature is related to. 13 | description: You may select more than one. 14 | multiple: true 15 | options: 16 | - stream_chat 17 | - stream_chat_flutter 18 | - stream_chat_flutter_core 19 | - stream_chat_persistance 20 | - stream_chat_localizations 21 | validations: 22 | required: true 23 | - type: dropdown 24 | id: platforms 25 | attributes: 26 | label: Which platforms would this feature impact? 27 | description: You may select more than one. 28 | multiple: true 29 | options: 30 | - iOS 31 | - Android 32 | - Web 33 | - Windows 34 | - MacOS 35 | - Linux 36 | - type: textarea 37 | id: problem 38 | attributes: 39 | label: Is your feature request related to a problem? 40 | description: A clear description of what the problem is. 41 | placeholder: "Example: I'm always frustrated when [...]" 42 | - type: textarea 43 | id: solution 44 | attributes: 45 | label: "Describe the solution that you'd like." 46 | description: A clear description of what you want to happen. 47 | placeholder: "Example: When clicking this I want that." 48 | - type: textarea 49 | id: alternatives 50 | attributes: 51 | label: "Describe alternatives that you have considered" 52 | description: "A clear description of any alternative solutions or features you've considered." 53 | placeholder: "Example: Instead of this it should do that." 54 | - type: textarea 55 | id: additional 56 | attributes: 57 | label: "Additional context" 58 | description: "Add any other context or screenshots about the feature request here." 59 | - type: checkboxes 60 | id: terms 61 | attributes: 62 | label: Code of Conduct 63 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/GetStream/stream-chat-flutter/blob/develop/CODE_OF_CONDUCT.md) 64 | options: 65 | - label: "I agree to follow this project's Code of Conduct" 66 | required: true 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | build 14 | 15 | node_modules 16 | classes.json 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # android-trace 2 | [Frida-node](https://github.com/frida/frida-node) CLI tool for automating Android class and method tracing. 3 | ### Installation 4 | android-trace requires the following: 5 | * [Node.js](https://nodejs.org/) v4+ to run, including node package manager NPM. 6 | * [Android Platform Tools](https://developer.android.com/studio/releases/platform-tools.html), specifically ADB (Android Debug Bridge). ADB must be added to the PATH environment. 7 | 8 | Installing Node for Debian and Ubuntu based Linux distributions: 9 | ```sh 10 | $ curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 11 | $ sudo apt-get install -y nodejs 12 | ``` 13 | Installing android-trace: 14 | ```sh 15 | $ git clone https://github.com/postgor/android-trace 16 | $ cd android-trace 17 | $ npm install 18 | ``` 19 | 20 | ### Setup 21 | ##### Frida-Server 22 | See [frida android](https://www.frida.re/docs/android/) for the full documentation on how to get Frida up and running for Android. 23 | 24 | Download the latest frida-server from [frida-releases](https://github.com/frida/frida/releases) for your Android platfrom architecture. 25 | 26 | Extract xz archive 27 | ```sh 28 | $ xz -d frida-server-{version}-android-{architecture}.xz {emulator IP addres} 29 | ``` 30 | 31 | Get frida-server running on your android device/emulator 32 | ```sh 33 | $ adb root # might be required 34 | $ adb push frida-server /data/local/tmp/ 35 | $ adb shell "chmod 755 /data/local/tmp/frida-server" 36 | $ adb shell "/data/local/tmp/frida-server &" 37 | ``` 38 | 39 | If you prefer to connect to the frida-server over a network instead of USB, substititute: 40 | ```sh 41 | $ adb shell "/data/local/tmp/frida-server &" 42 | ``` 43 | for: 44 | ```sh 45 | $ adb shell "/data/local/tmp/frida-server -l {device-ip:listening-port}" 46 | ``` 47 | 48 | Finally, make sure android-trace is working. As a test, see if you can list the device's running processes. 49 | Over USB: 50 | ```sh 51 | $ node index.js -U -r} 52 | ``` 53 | Over HOST: 54 | ```sh 55 | $ node index.js -H {device-ip:listening-port} -r 56 | ``` 57 | 58 | ### Ussage 59 | 60 | ##### Hook all the things 61 | Proceed with caution - generally it is not a good idea to hook every single class/method. Provide the application's package name: 62 | ```sh 63 | $ node index.js -U -n {package name} 64 | ``` 65 | or substitute package name for process id: 66 | ```sh 67 | $ node index.js -U -p {pid} 68 | ``` 69 | ##### Filtering by class 70 | The package com.androidtrace.test will be used as an example. Pretend this package has the following classes and methods: 71 | - myClass1 [myMethod1, myMethod2, myMethod3] 72 | - myClass2 [myMethod1, myMethod2, myMethod3] 73 | - myClass3 [myMethod1, myMethod2, myMethod3] 74 | 75 | Specify -F to only include classes that match a provided regex. A good idea would be to filter by package name or a name unique to the application's naming convention. For example, the below will hook all the classes that have the class-path "com.androidtrace.test". 76 | 77 | ```sh 78 | $ node index.js -U -n {package name} -F "com.androidtrace.test" 79 | ``` 80 | 81 | If you only want to hook "myClass1", you can apply one of the following filters: 82 | - "com.androidtrace.test.myClass1" 83 | - "androidtrace.*myClass1" 84 | - "myClass1" 85 | 86 | Note that a filter such as "myClass1" will hook other class-paths containing a match for "myClass1". For example, "com.android.myClass1SomethingElse". 87 | 88 | To include multiple filters, you can apply normal regex rules, for example: 89 | ```sh 90 | $ node index.js -U -n {package name} -F "myClass1|myClass2" 91 | ``` 92 | This will find matches for both "myClass1" and "myClass2". 93 | 94 | ##### Filtering by function 95 | 96 | The same rules for filtering by class name apply to filtering by method. Specify "-f" to filter by method name. For example to only hook the "myMethod2" function of the "myClass3" class, the following filter can be applied: 97 | ```sh 98 | $ node index.js -U -n {package name} -F "androidtrace.*myClass3" -f "myMethod2" 99 | ``` 100 | 101 | Note that you can also exclude the class filter (-F), however this will result in the script calling "Frida.use" on every enumerated class in order to discover the available methods. This could result in performance issues, and errors. It is best practice to always supply a class filter. 102 | 103 | ##### Excluding classes and methods 104 | 105 | Same rules as above, only inverse. For example, hook all the classes with a certain class-path, except myClass2: 106 | ```sh 107 | $ node index.js -U -n {package name} -F "com.androidtrace.test" -E "myClass2" 108 | ``` 109 | 110 | Hook all the methods in a certain class except "myMethod1" and "myMethod2": 111 | ```sh 112 | $ node index.js -U -n {package name} -F "com.androidtrace.test" -e "myMethod1|myMethod2" 113 | ``` 114 | 115 | ##### Trace classes from provided file 116 | android-trace allows you to provide a "json" file containing a list of classes that you want to trace. For example: 117 | 118 | ```sh 119 | $ node index.js -U -n {package name} -l classes.json 120 | ``` 121 | 122 | ["com.androidtrace.test.MyClass1","com.androidtrace.test.MyClass2"] 123 | -------------------------------------------------------------------------------- /agent.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let classHandle = {}; 4 | let method_filter; 5 | let method_exclude; 6 | let method_exclude_set = false; 7 | 8 | rpc.exports = { 9 | setMethodFilter: function (filter) { 10 | method_filter = new RegExp(filter); 11 | }, 12 | setMethodExlude: function(filter) { 13 | if (filter) 14 | method_exclude_set = true; 15 | method_exclude = new RegExp(filter); 16 | }, 17 | enumerateClasses: function() { 18 | startEnumerateClasses(); 19 | }, 20 | providedClassesHook: function(providedClasses) { 21 | startProvidedClassesHook(providedClasses); 22 | } 23 | } 24 | 25 | function startEnumerateClasses() { 26 | if (Java.available) { 27 | Java.perform(function(){ 28 | try{ 29 | Java.enumerateLoadedClasses({ 30 | onMatch: function(class_discovered) { 31 | send({ type: "class_discovered", data: class_discovered }); 32 | }, 33 | onComplete: function() { 34 | } 35 | }); 36 | } catch (err) { 37 | send({ type: "errorGeneric", data: "Java.perform error" }); 38 | console.error(err); 39 | } 40 | }); 41 | } else { 42 | send({ type: "errorGeneric", data: "Java.available error" }); 43 | } 44 | } 45 | 46 | function startProvidedClassesHook(providedClasss){ 47 | //TODO performNow or perform? 48 | Java.performNow(function(){ 49 | providedClasss.map(function(classNameToHook){ 50 | hookClass(classNameToHook); 51 | }); 52 | }); 53 | } 54 | 55 | 56 | /******CLASS HOOK FUNCTION******/ 57 | 58 | function hookClass(classNameToHook){ 59 | 60 | try { 61 | classHandle = Java.use(classNameToHook); 62 | } catch (err) { 63 | send({ type: "errorGeneric", data: "Java.use error in class: " + 64 | classNameToHook + " - skipping class" }); 65 | console.error(err); 66 | return; 67 | } 68 | 69 | /*HOOK CONSTRUCTORS*/ 70 | try { 71 | if (classHandle.$init.overloads.length > 0) { 72 | hookConstructors(classNameToHook); 73 | } else { 74 | send({ type: "info", data: "No constructor to hook in class: " + classNameToHook }); 75 | } 76 | } catch (err) { 77 | console.error(err); 78 | } 79 | 80 | /*HOOK FUNCTIONS*/ 81 | var allPropertyNames = getAllPropertyNames(classHandle); 82 | var allFunctionNames = getAllFunctionNames(allPropertyNames); 83 | 84 | allFunctionNames.map(function(methodNameToHook){ 85 | /*return if method name matches user exlcude regex*/ 86 | if(method_exclude_set && method_exclude.test(methodNameToHook)){ 87 | return; 88 | } 89 | /*perform hook if method name matches user filter regex*/ 90 | if(method_filter.test(methodNameToHook)){ 91 | try{ 92 | if (!(classHandle[methodNameToHook].overloads.length > 1)){ 93 | hookMethod(classNameToHook, methodNameToHook); 94 | } else { 95 | hookOverloadedMethod(classNameToHook, methodNameToHook); 96 | } 97 | } catch (err) { 98 | console.error(err); 99 | } 100 | } 101 | }); 102 | } 103 | 104 | 105 | /* 106 | METHOD AND CONSTRUCTOR HOOK FUNCTIONS 107 | */ 108 | function hookConstructors(classNameToHook){ 109 | var constructorMethods = classHandle.$init.overloads; 110 | for (var i in constructorMethods){ 111 | var argTypes = constructorMethods[i].argumentTypes.map(function(a) {return a.className;}); 112 | try{ 113 | send({ 114 | type: "constructorHooked", 115 | data: { 116 | methodType: "CONSTRUCTOR", 117 | className: classNameToHook, 118 | args: argTypes 119 | } 120 | }); 121 | 122 | classHandle.$init.overload.apply(this, argTypes).implementation = function() { 123 | 124 | var args = Array.prototype.slice.call(arguments); 125 | // send message on hook 126 | send({ 127 | type: "constructorCalled", 128 | data: { 129 | methodType: "CONSTRUCTOR", 130 | className: classNameToHook, 131 | // args: JSON.stringify(args) 132 | argTypes: argTypes, 133 | args: args + "" 134 | } 135 | }); 136 | 137 | return this.$init.apply(this, args); 138 | } 139 | } catch (err){ 140 | console.error(err); 141 | } 142 | } 143 | } 144 | 145 | function hookMethod(classNameToHook, methodNameToHook){ 146 | var argTypes = classHandle[methodNameToHook].argumentTypes.map(function(a) {return a.className;}); 147 | try{ 148 | // send message to indicate the method is being hooked 149 | send({ 150 | type: "methodHooked", 151 | data: { 152 | methodType: "METHOD", 153 | className: classNameToHook, 154 | methodName: methodNameToHook, 155 | args: argTypes 156 | } 157 | }); 158 | 159 | classHandle[methodNameToHook].implementation = function() { 160 | var args = Array.prototype.slice.call(arguments); 161 | var retVal = this[methodNameToHook].apply(this, args); 162 | // send message on hook 163 | send({ 164 | type: "methodCalled", 165 | data: { 166 | methodType: "METHOD", 167 | className: classNameToHook, 168 | methodName: methodNameToHook, 169 | argTypes: argTypes, 170 | // args: JSON.stringify(args) 171 | args: args + "", 172 | ret: retVal + "" 173 | } 174 | }); 175 | 176 | return retVal; 177 | }; 178 | }catch (err){ 179 | send({ 180 | type: "errorHook", 181 | data: { 182 | methodType: "METHOD", 183 | className: classNameToHook, 184 | methodName: methodNameToHook, 185 | args: argTypes 186 | } 187 | }); 188 | console.error(err); 189 | } 190 | } 191 | 192 | function hookOverloadedMethod(classNameToHook, methodNameToHook){ 193 | var overloadedMethods = classHandle[methodNameToHook].overloads; 194 | for (var i in overloadedMethods){ 195 | var argTypes = overloadedMethods[i].argumentTypes.map(function(a) {return a.className;}); 196 | try{ 197 | // send message to indicate the overloaded method is being hooked 198 | send({ 199 | type: "methodHooked", 200 | data: { 201 | methodType: "OVERLOADED METHOD", 202 | className: classNameToHook, 203 | methodName: methodNameToHook, 204 | args: argTypes 205 | } 206 | }); 207 | 208 | classHandle[methodNameToHook].overload.apply(this, argTypes).implementation = function() { 209 | 210 | var args = Array.prototype.slice.call(arguments); 211 | var retVal = this[methodNameToHook].apply(this, args); 212 | // send message on hook 213 | send({ 214 | type: "methodCalled", 215 | data: { 216 | methodType: "OVERLOADED METHOD", 217 | className: classNameToHook, 218 | methodName: methodNameToHook, 219 | // argTypes: argTypes, 220 | // args: JSON.stringify(args) 221 | args: args + "", 222 | ret: retVal + "" 223 | 224 | } 225 | }); 226 | 227 | // return this[methodNameToHook].apply(this, args); 228 | return retVal; 229 | }; 230 | } catch (err){ 231 | send({ 232 | type: "errorHook", 233 | data: { 234 | methodType: "OVERLOADED METHOD", 235 | className: classNameToHook, 236 | methodName: methodNameToHook, 237 | args: argTypes 238 | } 239 | }); 240 | console.error(err); 241 | } 242 | } 243 | } 244 | 245 | 246 | /* 247 | CUSTOM FUNCTIONS 248 | */ 249 | 250 | /* 251 | return all the property names for an object by walking up the prototype chain 252 | enum/nonenum, self/inherited.. 253 | */ 254 | function getAllPropertyNames( obj ) { 255 | var props = []; 256 | 257 | do { 258 | props= props.concat(Object.getOwnPropertyNames( obj )); 259 | } while ( obj = Object.getPrototypeOf( obj ) ); 260 | 261 | return props; 262 | } 263 | 264 | /*cheap hack to only get the function names of the intended class*/ 265 | //TODO do it better I guess 266 | function getAllFunctionNames( propertyNames ) { 267 | var begin_pos = propertyNames.indexOf("$className"); 268 | var end_pos = propertyNames.indexOf("constructor", begin_pos); 269 | var functionNames = propertyNames.slice(begin_pos+1, end_pos); 270 | return functionNames.filter(function(funcName){ 271 | if (typeof(classHandle[funcName]) === "function"){ 272 | return funcName; 273 | } 274 | }); 275 | } 276 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const co = require('co'); 4 | const frida = require('frida'); 5 | const load = require('frida-load'); 6 | const program = require('commander'); 7 | const agent_handler = require('./modules/agentHandler'); 8 | 9 | /* 10 | PARSE COMMAND LINE OPTIONS 11 | */ 12 | function list(val) { 13 | return val.split(','); 14 | } 15 | 16 | //TODO sort out these 17 | program 18 | .version('1.0.0') 19 | .option('-U, --usb', 'connect to USB device') 20 | .option('-H --host ','connect to remote frida-server on HOST') 21 | .option('-n, --package-name ', 'android application package name') 22 | .option('-p, --attach-pid ', 'attach to PID', parseInt) 23 | .option('-F, --filter-class ', 'specify regex filter for classes to include in trace. The class path will be included as part of the string to be filtered, ex. "com.test.ClassName"') 24 | .option('-f, --filter-method ', 'specify regex filter for methods to include in trace') 25 | .option('-E, --exclude-class ', 'comma seperated list of class names to exclude, e.g. -E ClassName1,ClassName2') 26 | .option('-e, --exclude-method ', 'comma seperated list of method names to exclude, e.g. -e methodName1,methodName2') 27 | .option('-l, --load-classes ', 'load classes specified in provided file') 28 | .option('-d, --discover', 'do not perform any tracing, only enumerate classes at run-time and dump to file - filters/exludes apply') 29 | .option('-t, --time ', 'specify the time in seconds between each class enumeration call, default is 30 seconds', parseInt) 30 | // .option('-o, --out', 'specify output file for enumerated classes, default is "classes.json"') 31 | .option('-r, --running-processes', 'list running processes') 32 | .parse(process.argv); 33 | 34 | 35 | if (!process.argv.slice(2).length) { 36 | program.outputHelp(); 37 | process.exit(1); 38 | } 39 | 40 | 41 | let host = ""; 42 | let filter_class_include = ""; 43 | let filter_class_exclude = ""; 44 | let filter_method = ""; 45 | let exclude_method = ""; 46 | let file_name = ""; 47 | let time = 30000; 48 | if (program.host) 49 | host = program.host; 50 | if (program.filterClass) 51 | filter_class_include = program.filterClass; 52 | if (program.excludeClass) 53 | filter_class_exclude = program.excludeClass; 54 | if (program.filterMethod) 55 | filter_method = program.filterMethod; 56 | if (program.excludeMethod) 57 | exclude_method = program.excludeMethod; 58 | if (program.loadClasses) 59 | file_name = program.loadClasses; 60 | if (program.time){ 61 | time = program.time * 1000; 62 | if (time < 5000) { 63 | console.error("\nTime '-t' can not be less than 5 seconds\n"); 64 | process.exit(1); 65 | } 66 | } 67 | 68 | 69 | const package_name = program.packageName; 70 | const attachPid = program.attachPid; 71 | 72 | 73 | /* 74 | PRINT STATE INFORMATION 75 | */ 76 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Package name: ' + package_name }); 77 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Class include filter: ' + new RegExp(filter_class_include)}); 78 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Method include filter: ' + new RegExp(filter_method)}); 79 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Class exclude filter: ' + new RegExp(filter_class_exclude)}); 80 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Method exclude filter: ' + new RegExp(exclude_method)}); 81 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Provided classes file: ' + file_name }); 82 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Time between enumeration: ' + time/1000 + ' seconds' }); 83 | 84 | 85 | /* 86 | INIT AGENT AND AGENTHANDLER 87 | */ 88 | co(function *() { 89 | 90 | /* 91 | "-U", connect over USB 92 | "-H", connect over HOST - {ip}:{port} 93 | else print error message and exit 94 | */ 95 | let device; 96 | if (program.usb){ 97 | device = yield frida.getUsbDevice(1); 98 | } else if (program.host){ 99 | const mgr = frida.getDeviceManager(); 100 | device = yield mgr.addRemoteDevice(host); 101 | } else { 102 | console.error("\n Please specify USB '-U' or HOST '-H {ip}:{port}' to connect to a device\n") 103 | process.exit(1); 104 | } 105 | 106 | 107 | /* 108 | "-r", print running processes on the device and exit 109 | */ 110 | if (program.runningProcesses){ 111 | let processes = yield device.enumerateProcesses(); 112 | printRunningProcesses(processes); 113 | process.exit(1); 114 | } 115 | 116 | 117 | /* 118 | "-n", attach to provided package name 119 | "-p", attach to provided pid 120 | else print error message and exit 121 | */ 122 | let session; 123 | if (program.packageName){ 124 | session = yield device.attach(package_name); 125 | } else if (program.attachPid){ 126 | session = yield device.attach(attachPid); 127 | } else { 128 | console.error("\n Missing arguments\n"); 129 | process.exit(1); 130 | } 131 | 132 | 133 | /*create agent script*/ 134 | const scr = yield load(require.resolve('./agent.js')); 135 | const script = yield session.createScript(scr); 136 | 137 | 138 | /*load agent script and get script exported components*/ 139 | yield script.load(); 140 | const agent_api = yield script.getExports(); 141 | 142 | 143 | /*set agent handler fields*/ 144 | agent_handler.handler.setAgentApi(agent_api); 145 | agent_handler.handler.setClassFilter(filter_class_include); 146 | agent_handler.handler.setClassExclude(filter_class_exclude); 147 | 148 | 149 | /*create event listener -> call agent message handler*/ 150 | script.events.listen('message', agent_handler.handler.agentMessageHandler); 151 | 152 | 153 | /*set agent fields*/ 154 | yield agent_api.setMethodFilter(filter_method); 155 | // yield agent_api.setExcludeClassNames(filter_class_exclude); 156 | yield agent_api.setMethodExlude(exclude_method); 157 | 158 | 159 | /* 160 | "-d", do not perform any tracing, only enumerate classes and dump to file for later use - filters/excludes apply 161 | */ 162 | if (program.discover){ 163 | agent_handler.handler.setEnumerateOnly(); 164 | agent_handler.handler.printStateInformation({ type: 'info', data: 'Enumerating classes every ' + time/1000 + ' seconds' }); 165 | 166 | } 167 | 168 | /* 169 | make sure that the user does not specify both "-l" and "-d" 170 | "-d" only enumerate classes and do not perform tracing 171 | "-l" only trace the classes in the provided file, without any enumeration 172 | */ 173 | if (program.discover && program.loadClasses) { 174 | console.error("\n Can not specify both '-l' and '-d' at the same time\n"); 175 | process.exit(1); 176 | } 177 | 178 | 179 | /* 180 | "-l", hook classes in provided json file, format -> ["com.test.Class1","com.test.Class2"] 181 | else enumerate classes 182 | */ 183 | if (program.loadClasses){ 184 | agent_handler.handler.traceClassesFromFile(file_name); 185 | } else { 186 | /*enumerate and hook classes at runtime*/ 187 | yield agent_api.enumerateClasses() 188 | .then(function() { 189 | agent_handler.handler.enumerateClassesDone(); 190 | }); 191 | 192 | /*enumerate classes every fixed interval to discover new loaded classes*/ 193 | setInterval(enumClasses, time, agent_api); 194 | } 195 | 196 | 197 | /*display message to indicate that the script has finished loading*/ 198 | agent_handler.handler.printStateInformation({ type: "info", data: "Script loaded\n" }); 199 | }) 200 | .catch(err => { 201 | console.error(err); 202 | }); 203 | 204 | 205 | /*function called by setInterval to enumerate classes every fixed interval*/ 206 | function enumClasses(agent_api){ 207 | agent_api.enumerateClasses().then(function() { 208 | agent_handler.handler.enumerateClassesDone(); 209 | });; 210 | } 211 | 212 | 213 | /*function to pretty print running processes*/ 214 | function printRunningProcesses(processes){ 215 | processes.map(function(element){ 216 | console.log(" pid: " + element.pid + " ; " + "name: " + element.name); 217 | }) 218 | } 219 | -------------------------------------------------------------------------------- /modules/agentHandler.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | const co = require('co'); 3 | const fs = require('fs'); 4 | 5 | handler = (function(){ 6 | 7 | /* 8 | CHALK LOCAL VARIABLES 9 | */ 10 | const chalk_error = chalk.bold.red; 11 | const chalk_state = chalk.yellow; 12 | const chalk_info = chalk.blue; 13 | const chalk_infoAlternative = chalk.gray; 14 | const chalk_data = chalk.green; 15 | const chalk_dataAlternative = chalk.cyan; 16 | 17 | /* 18 | LOCAL VARIABLES 19 | */ 20 | let agent_api = {}; 21 | let enumerated_classes = {} 22 | let classes_that_match_filter = [] 23 | let new_classes_to_hook = []; 24 | let enumerate_only = false; 25 | let class_exclude_set = false; 26 | let class_filter_include; 27 | let class_filter_exclude; 28 | 29 | /* 30 | LOCAL FUNCTIONS 31 | */ 32 | 33 | function classDiscovered(class_name){ 34 | var test = "/^(?!.*foobar)/"; 35 | if(!enumerated_classes[class_name]){ 36 | enumerated_classes[class_name] = true; // object for searching 37 | if (filterClass(class_name)) { 38 | classes_that_match_filter.push(class_name); // array for dumping to discovered classes file 39 | new_classes_to_hook.push(class_name); // new classes discovered that need to be hooked when enumerate classes is finished 40 | } 41 | } 42 | } 43 | 44 | function outputClassesToFile(){ 45 | fs.writeFile('./classes.json', JSON.stringify(classes_that_match_filter), 46 | function (err) { 47 | if (err) { 48 | console.error(err); 49 | } 50 | } 51 | ); 52 | } 53 | 54 | /*filter class name by user supplied regex - exclude/include*/ 55 | function filterClass(class_name){ 56 | return ((!class_exclude_set || !class_filter_exclude.test(class_name)) && class_filter_include.test(class_name)) 57 | } 58 | 59 | function printLineBreak(){ 60 | console.log(" --------------------------------------------------------------"); 61 | } 62 | 63 | /* 64 | PUBLIC FUNCTIONS 65 | */ 66 | 67 | function setAgentApi(api){ 68 | agent_api = api; 69 | } 70 | 71 | function setClassFilter(filter_val){ 72 | class_filter_include = new RegExp(filter_val); 73 | } 74 | 75 | function setClassExclude(exclude_val){ 76 | //TODO below is a negative look-ahead - Test out the current regex exlcudes method, else revert to below 77 | // let pattern = ""; 78 | // if (exclude_val){ 79 | // pattern = "^(?!" + exclude_val + ")" 80 | // } 81 | // class_filter_exclude = new RegExp(pattern); 82 | if (exclude_val) 83 | class_exclude_set = true; 84 | class_filter_exclude = new RegExp(exclude_val); 85 | } 86 | 87 | function setEnumerateOnly() { 88 | enumerate_only = true; 89 | } 90 | 91 | function printStateInformation(message){ 92 | if (message.type === "info") { 93 | console.log("\n " + chalk_state(message.data)); 94 | } 95 | } 96 | 97 | function traceClassesFromFile(file){ 98 | fs.readFile(file, 'utf8', function (err, data) { 99 | if (err) 100 | throw err; 101 | let classes_from_file = JSON.parse(data); 102 | let classes_to_hook = classes_from_file.filter(filterClass); 103 | let message = " Finished loading classes from file: hooking " + classes_to_hook.length + " classes"; 104 | console.log("\n" + chalk_state(message)); 105 | if (classes_to_hook.length) 106 | agent_api.providedClassesHook(classes_to_hook); 107 | }); 108 | } 109 | 110 | function enumerateClassesDone(){ 111 | let message = " Finished enumerating classes: discovered " + new_classes_to_hook.length + " classes"; 112 | console.log("\n" + chalk_state(message)); 113 | /*if new classes to be hooked are discovered, and the user is performing tracing*/ 114 | if(new_classes_to_hook.length > 0 && !enumerate_only){ 115 | agent_api.providedClassesHook(new_classes_to_hook) 116 | .then((data) => console.log(chalk_state("\n Finished hooking the discovered methods"))) 117 | .catch((err) => { 118 | console.log(err); 119 | console.log(chalk_state("\nThis is a 'Frida' issue (not 'android-trace'). If you see this error it would be a good idea to apply an exclude filter for this specific class and try again.\n")); 120 | }); 121 | } 122 | 123 | new_classes_to_hook = []; // clear new_classes_to_hook after the classes have been hooked 124 | outputClassesToFile(); // output all hooked classes to a file 125 | } 126 | 127 | function agentMessageHandler(message){ 128 | if ("undefined" !== typeof message.payload){ 129 | switch(message.payload.type) { 130 | case "class_discovered": 131 | classDiscovered(message.payload.data); 132 | break; 133 | case "methodCalled": 134 | printLineBreak(); 135 | console.log(chalk_data(" CLASS ") + message.payload.data.className); 136 | console.log(chalk_data(" " + message.payload.data.methodType)+ " " + message.payload.data.methodName); 137 | // console.log(chalk_data(" ARGUMENT TYPES: ") + message.payload.data.argTypes); 138 | console.log(chalk_data(" ARGUMENTS: ") + message.payload.data.args); 139 | console.log(chalk_data(" RETURN: ") + message.payload.data.ret); 140 | break; 141 | case "constructorCalled": 142 | printLineBreak(); 143 | console.log(chalk_dataAlternative(" CONSTRUCTOR ") + message.payload.data.className); 144 | console.log(chalk_dataAlternative(" ARGUMENT TYPES: ") + message.payload.data.argTypes); 145 | console.log(chalk_dataAlternative(" ARGUMENTS: ") + message.payload.data.args); 146 | break; 147 | case "constructorHooked": 148 | printLineBreak(); 149 | console.log(chalk_info(" CONSTRUCTOR ") + message.payload.data.className); 150 | console.log(chalk_info(" ARGUMENT TYPES: ") + message.payload.data.args); 151 | break; 152 | case "methodHooked": 153 | printLineBreak(); 154 | console.log(chalk_info(" CLASS ") + message.payload.data.className); 155 | console.log(chalk_info(" " + message.payload.data.methodType)+ " " + message.payload.data.methodName); 156 | console.log(chalk_info(" ARGUMENT TYPES: ") + message.payload.data.args); 157 | break; 158 | case "errorHook": 159 | printLineBreak(); 160 | console.log(chalk_error(" CLASS ") + message.payload.data.className); 161 | console.log(chalk_error(" " + message.payload.data.methodType)+ " " + message.payload.data.methodName); 162 | console.log(chalk_error(" ARGUMENT TYPES: ") + message.payload.data.args); 163 | break; 164 | case "info": 165 | printLineBreak(); 166 | console.log("\n " + chalk_state(message.payload.data)); 167 | break; 168 | case "errorGeneric": 169 | printLineBreak(); 170 | console.log("\n " + chalk_state(message.payload.data)); 171 | break; 172 | } 173 | } 174 | } 175 | 176 | /* 177 | RETURN OBJECTS 178 | */ 179 | 180 | return { 181 | setAgentApi, 182 | setClassFilter, 183 | setClassExclude, 184 | setEnumerateOnly, 185 | agentMessageHandler, 186 | traceClassesFromFile, 187 | enumerateClassesDone, 188 | printStateInformation 189 | } 190 | 191 | })() 192 | 193 | module.exports = { 194 | handler: handler 195 | } 196 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "android-trace", 3 | "version": "1.0.0", 4 | "description": "Frida-node cli for Android class and method tracing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/postgor/android-trace.git" 12 | }, 13 | "author": "postgor", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/postgor/android-trace/issues" 17 | }, 18 | "homepage": "https://github.com/postgor/android-trace#readme", 19 | "dependencies": { 20 | "chalk": "^1.1.3", 21 | "co": "^4.6.0", 22 | "commander": "^2.9.0", 23 | "frida": "^10.0.9", 24 | "frida-load": "^1.0.0" 25 | } 26 | } 27 | --------------------------------------------------------------------------------