├── .gitignore ├── README.md ├── backend ├── backend │ ├── __init__.py │ ├── routing.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── db.sqlite3 ├── manage.py ├── requirements.txt └── server │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── functions │ ├── __init__.py │ ├── adb_parse.py │ ├── frida_main.py │ ├── frida_ps.py │ ├── frida_scripts │ │ ├── agent.js │ │ ├── custom.js │ │ ├── dexDump.js │ │ └── fridaAPI.js │ ├── frida_unpack.py │ └── utils.py │ ├── migrations │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── views.py │ └── ws.py └── frontend ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── Router ├── config.js └── router.js ├── index.css ├── index.js ├── loading └── loader.js ├── logo.svg ├── serviceWorker.js ├── setupTests.js └── views ├── appInfo.js ├── appUnpack.js ├── home.js ├── javaEnum.js ├── javaNativeTrace.js ├── javaTODO.js ├── nativeEnum.js ├── nativeTODO.js └── siderMenu.js /.gitignore: -------------------------------------------------------------------------------- 1 | frontend/node_modules 2 | frontend/build 3 | backend/server/output 4 | **/__pycache__ 5 | release/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # frider 2 | 3 | Dump dex, trace/intercept Java/native function. Based on React, Django, Frida, adb. 4 | 5 | ## Features 6 | 7 | - enumerate Android APPs 8 | - show basic information 9 | - unpack app, dump dex 10 | - enumerate Java classes & methods 11 | - enumerate Native modules & export functions/variables 12 | - trace arguments & retval of Java methods (needs Burp to intercept) 13 | - trace arguments & retval of native function (not really, requires user to finish related arguments parsing) 14 | 15 | ## Install 16 | 17 | Make sure you are using **latest** frida-tools and frida-server (test on 12.11.9). 18 | 19 | ``` 20 | (dev version) 21 | git clone https://github.com/refate/frider 22 | cd backend && pip3 install -r requirements.txt 23 | cd frontend && npm install 24 | 25 | (release version) 26 | download latest release, and extract it somewhere. 27 | ``` 28 | 29 | ## Usage 30 | 31 | Please refer to [wiki page](https://github.com/refate/frider/wiki/Usage) 32 | 33 | [中文版wiki](https://github.com/refate/frider/wiki/Usage-CN) 34 | 35 | ## Reference & Thanks 36 | 37 | [monkeylord/XServer](https://github.com/monkeylord/XServer) 38 | 39 | [viva-frida/Awesome--Frida-UI](https://github.com/viva-frida/Awesome--Frida-UI) 40 | 41 | [GuoQiang1993/Frida-Apk-Unpack](https://github.com/GuoQiang1993/Frida-Apk-Unpack) 42 | 43 | [hluwa/FRIDA-DEXDump](https://github.com/hluwa/FRIDA-DEXDump) 44 | 45 | [T3rry7f/Hijacker](https://github.com/T3rry7f/Hijacker) 46 | 47 | [Ant Design](https://ant.design/) 48 | 49 | ## TODO 50 | 51 | bug fix 52 | 53 | ## Change log 54 | 55 | 2020/08/24 v0.1.1 test release -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/backend/backend/__init__.py -------------------------------------------------------------------------------- /backend/backend/routing.py: -------------------------------------------------------------------------------- 1 | # channels routing 2 | from channels.auth import AuthMiddlewareStack 3 | from channels.routing import ProtocolTypeRouter, URLRouter 4 | # from server.ws import ChatConsumer 5 | from server.functions.frida_main import ChatConsumer 6 | 7 | from django.conf.urls import url 8 | 9 | application = ProtocolTypeRouter({ 10 | "websocket": AuthMiddlewareStack( 11 | URLRouter([ 12 | url("api/ws/", ChatConsumer), 13 | ]) 14 | ), 15 | }) -------------------------------------------------------------------------------- /backend/backend/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '%a9ucs5&gj1eq+4zc&i@ue060tiqj$4$m#39ehvsrdwfp^&thy' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'corsheaders', 42 | 'backend', 43 | 'channels', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'corsheaders.middleware.CorsMiddleware', # new 50 | 'django.middleware.common.CommonMiddleware', 51 | #'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | # WEBSOCKET_ACCEPT_ALL = True 58 | ROOT_URLCONF = 'backend.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'backend.wsgi.application' 77 | ASGI_APPLICATION = 'backend.routing.application' 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | 128 | CORS_ALLOW_CREDENTIALS = True 129 | CORS_ORIGIN_WHITELIST = ( 130 | 'http://127.0.0.1:3000', 131 | 'https://127.0.0.1:3000', 132 | 'http://localhost:3000', 133 | 'https://localhost:3000', 134 | ) 135 | 136 | CORS_ALLOW_METHODS = [ 137 | 'DELETE', 138 | 'GET', 139 | 'OPTIONS', 140 | 'PATCH', 141 | 'POST', 142 | 'PUT', 143 | ] 144 | 145 | CORS_ALLOW_HEADERS = [ 146 | 'accept', 147 | 'accept-encoding', 148 | 'authorization', 149 | 'content-type', 150 | 'dnt', 151 | 'origin', 152 | 'user-agent', 153 | 'x-csrftoken', 154 | 'x-requested-with', 155 | ] -------------------------------------------------------------------------------- /backend/backend/urls.py: -------------------------------------------------------------------------------- 1 | """backend URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | from server import views 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('api/getAppList/', views.getAppList, name='getAppList'), 23 | path('api/getAppInfo/', views.getAppInfo, name='getAppInfo'), 24 | path('api/appUnpack/', views.appUnpack, name='appUnpack'), 25 | path('api/injectApp/', views.injectApp, name='injectApp'), 26 | path('api/enumJavaClasses/', views.enumJavaClasses, name='enumJavaClasses'), 27 | path('api/enumJavaMethods/', views.enumJavaMethods, name='enumJavaMethods'), 28 | path('api/enumNativeModules/', views.enumNativeModules, name='enumNativeModules'), 29 | path('api/enumNativeExports/', views.enumNativeExports, name='enumNativeExports'), 30 | path('api/traceList/', views.traceList, name='traceList'), 31 | path('api/traceByAddress/', views.traceByAddress, name='traceByAddress'), 32 | ] 33 | -------------------------------------------------------------------------------- /backend/backend/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/backend/db.sqlite3 -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/backend/requirements.txt -------------------------------------------------------------------------------- /backend/server/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/backend/server/__init__.py -------------------------------------------------------------------------------- /backend/server/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/server/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ServerConfig(AppConfig): 5 | name = 'server' 6 | -------------------------------------------------------------------------------- /backend/server/functions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/backend/server/functions/__init__.py -------------------------------------------------------------------------------- /backend/server/functions/adb_parse.py: -------------------------------------------------------------------------------- 1 | import json 2 | import threading 3 | from server.functions import utils 4 | 5 | allInfo = '' # (string) all information of one app 6 | len_allInfo = 0 # length of allInfo 7 | result = {} # result dictionary (retval) 8 | lock = threading.Lock() # muti-threads 9 | threads = [] 10 | 11 | # find infoTags and save to result(global dictionary) 12 | def findLine(infoTag): 13 | global allInfo, result, len_allInfo 14 | position_infoTag = allInfo.find(infoTag) 15 | if (position_infoTag == -1): # tag not found 16 | result[infoTag] = infoTag + "null" # save into result dictionary 17 | else: 18 | position_next_blank = allInfo.find('\r\n', position_infoTag, len_allInfo) # find row end 19 | result[infoTag] = allInfo[position_infoTag : position_next_blank] 20 | 21 | def findLine2(infoTag2): 22 | global allInfo, result, len_allInfo 23 | position_infoTag = allInfo.find(infoTag2) 24 | if (position_infoTag == -1): # tag not found 25 | result[infoTag2] = infoTag2 + "\tnull" # save into result dictionary 26 | else: 27 | position_next_area = allInfo.find('\r\n ', position_infoTag, len_allInfo) 28 | while (allInfo[position_next_area + 6] == ' '): 29 | # position_infoTag = position_next_area + 8 30 | position_next_area = allInfo.find('\r\n ', position_next_area + 1, len_allInfo) 31 | result[infoTag2] = allInfo[position_infoTag : position_next_area] 32 | 33 | def getAndroidVersion(): 34 | command = 'adb shell getprop ro.build.version.release' 35 | lines = utils.shell_execution(command) 36 | AndroidVersion = lines[0].strip() 37 | return {"errCode":0, "result": AndroidVersion} 38 | 39 | def _getAppInfo(packageName): 40 | global allInfo, result, len_allInfo 41 | # run adb shell dumpsys package in shell 42 | command = 'adb shell dumpsys package {}'.format(packageName) 43 | lines = utils.shell_execution(command) 44 | allInfo = ''.join(lines) # list to string 45 | len_allInfo = len(allInfo) 46 | infoTags = [ # whose value is in the same line 47 | "versionName=", 48 | "versionCode=", 49 | "minSdk=", 50 | "flags=", 51 | "privateFlags=", 52 | "lastUpdateTime=", 53 | "primaryCpuAbi=", 54 | "secondaryCpuAbi=", 55 | "Instruction Set:", 56 | "path:", 57 | "status:", 58 | "codePath=", 59 | "resourcePath=", 60 | "legacyNativeLibraryDir=", 61 | "dataDir=", 62 | ] 63 | infoTags2 = [ # whose value is in next lines 64 | "usesLibraryFiles", 65 | "usesOptionalLibraries", 66 | "declared permissions:", 67 | "requested permissions:", 68 | ] 69 | 70 | # muti-threads search infoTags 71 | for infoTag in infoTags: 72 | t = threading.Thread(target=findLine, args=(infoTag, )) 73 | threads.append(t) 74 | t.start() 75 | for infoTag2 in infoTags2: 76 | t = threading.Thread(target=findLine2, args=(infoTag2,)) 77 | threads.append(t) 78 | t.start() 79 | # wating for threads to end 80 | for t in threads: 81 | t.join() 82 | 83 | result_list = [ 84 | ('package', packageName), 85 | ('versionName', result['versionName='][12:]), 86 | ('versionCode', result['versionCode='][12:].split(' ')[0]), 87 | ('sdk', result['minSdk=']), 88 | ('flags', result['flags=']), 89 | ('privateFlags', result['privateFlags=']), 90 | ('lastUpdateTime', result['lastUpdateTime='][15:]), 91 | ('primaryCpuAbi', result['primaryCpuAbi=']), 92 | ('secondaryCpuAbi', result['secondaryCpuAbi=']), 93 | ('instructionSet', result['Instruction Set:']), 94 | ('path', result['path:']), 95 | ('status', result['status:']), 96 | ('codePath', result['codePath=']), 97 | ('resourcePath', result['resourcePath=']), 98 | ('legacyNativeLibraryDir', result['legacyNativeLibraryDir=']), 99 | ('dataDir', result['dataDir=']), 100 | ('usesLibraryFiles', result['usesLibraryFiles']), 101 | ('usesOptionalLibraries', result['usesOptionalLibraries']), 102 | ('declaredPermissions', result['declared permissions:']), 103 | ('requestedPermissions', result['requested permissions:']) 104 | ] 105 | return {"errCode":0, "result":(dict(result_list))} 106 | -------------------------------------------------------------------------------- /backend/server/functions/frida_main.py: -------------------------------------------------------------------------------- 1 | # _*_ coding:utf-8 _*_ 2 | import frida 3 | import json 4 | import time 5 | import threading 6 | from server.functions import utils 7 | 8 | import requests 9 | import urllib 10 | import multiprocessing 11 | from http.server import HTTPServer, BaseHTTPRequestHandler 12 | 13 | BURP_HOST="127.0.0.1" 14 | BURP_PORT=8080 15 | SERVER_HOST="127.0.0.1" 16 | SERVER_PORT=17042 17 | SERVER_PROCESS=None 18 | 19 | # Plan A 20 | api = object() # frida script exports 21 | packageName = '' # current injected package name 22 | consumer = object() 23 | script = object() # frida script object 24 | 25 | # intercept by burp 26 | class FridaProxy(BaseHTTPRequestHandler): 27 | def do_FRIDA(self): 28 | request_headers = self.headers 29 | content_length = request_headers.get('content-length') 30 | length = int(content_length) if content_length else 0 31 | self.send_response(200) 32 | self.send_header('Content-type', 'application/json') 33 | self.end_headers() 34 | data = self.rfile.read(length) 35 | self.wfile.write(data) 36 | 37 | def start(): 38 | server = HTTPServer((SERVER_HOST, SERVER_PORT), FridaProxy) 39 | print("[OK] Frida proxy server on ::%d started !" %SERVER_PORT) 40 | server.serve_forever() 41 | 42 | # get USB device, return frida object 43 | def get_usb_device(): 44 | dManager = frida.get_device_manager() 45 | changed = threading.Event() 46 | def on_changed(): 47 | changed.set() 48 | dManager.on('changed', on_changed) 49 | 50 | device = None 51 | while device is None: 52 | devices = [dev for dev in dManager.enumerate_devices() if dev.type == 'usb'] 53 | if len(devices) == 0: 54 | print('Waiting for usb device...') 55 | changed.wait() 56 | time.sleep(2) 57 | else: 58 | device = devices[0] 59 | 60 | dManager.off('changed', on_changed) 61 | return device 62 | 63 | def frida_receive(message, data): 64 | if message['type'] == 'send': 65 | if(message['payload'][:9] == 'Frider:::'): 66 | packet = json.loads(message['payload'][9:]) 67 | if (packet['cmd'] == 'proxy'): # intercept by burp 68 | api = packet['data'][0] 69 | message = json.dumps(packet['data'][1]) 70 | # print(message) 71 | req = requests.request('FRIDA', "http://%s:%d/%s" % (SERVER_HOST, SERVER_PORT, api),data=message,proxies={'http':'http://%s:%d' % (BURP_HOST, BURP_PORT)}) 72 | global script 73 | script.post({ 'type': 'burp', 'data': urllib.request.unquote(req.content.decode("utf-8"))}) 74 | else: 75 | global consumer # get consumer object 76 | ChatConsumer.websocket_send(consumer, packet) 77 | else: 78 | print(message['stack']) 79 | 80 | def frida_load_script(scriptName, package): 81 | global script 82 | scriptDir = './server/functions/frida_scripts/' 83 | try: 84 | scriptPath = scriptDir + scriptName 85 | with open(scriptPath, 'r', encoding='utf-8') as f: 86 | scriptCode = f.read() 87 | device = get_usb_device() 88 | pid = device.spawn([package]) 89 | session = device.attach(pid) 90 | script = session.create_script(scriptCode) 91 | script.on("message", frida_receive) # receive messages 92 | script.load() 93 | api = script.exports 94 | device.resume(pid) 95 | time.sleep(1) 96 | except Exception as ex: 97 | return ex 98 | return api 99 | 100 | def _injectApp(package): 101 | scriptName = 'fridaAPI.js' 102 | res = frida_load_script(scriptName, package) 103 | print(str(res)) 104 | #if (isinstance(res, object)): # 待测试 105 | if(type(res).__name__ == 'ScriptExports'): 106 | global api, packageName 107 | api = res 108 | packageName = package 109 | return {"errCode": 0 } 110 | else: 111 | return {"errCode": 1, "errMsg": str(res) } 112 | 113 | def _enumJavaClasses(): 114 | global api 115 | if( api.is_java_available == False ): # check Java VM 116 | return {"errCode": 2, "errMsg": "The current process doesn't have a Java VM loaded."} 117 | JavaClassNames = api.enumerate_classes() 118 | JavaClasses = list() 119 | key = 1 120 | for JavaClassName in JavaClassNames: 121 | JavaClass = { 122 | 'key': str(key), 123 | 'class': JavaClassName, 124 | } 125 | JavaClasses.append(JavaClass) 126 | key = key + 1 127 | return { "errCode": 0, "result": JavaClasses } 128 | 129 | def _enumJavaMethods(query): 130 | global api 131 | if( api.is_java_available == False ): # check Java VM 132 | return {"errCode": 2, "errMsg": "The current process doesn't have a Java VM loaded."} 133 | JavaMethods = api.enumerate_methods(query) 134 | return { "errCode": 0, "result": JavaMethods } 135 | 136 | def _enumNativeModules(): 137 | global api, packageName 138 | appOnly = [] 139 | result = [] 140 | NativeModules = api.enumerate_modules() 141 | key = 1 142 | for NativeModule in NativeModules: # pick out inside-app libraries 143 | NativeModule['key'] = str(key) 144 | if packageName in NativeModule["path"]: 145 | appOnly.append(NativeModule) 146 | key = key + 1 147 | result = { 148 | 'appOnly': appOnly, 149 | 'all': NativeModules, 150 | } 151 | return { "errCode": 0, "result": result } 152 | 153 | def _enumNativeExports(module): 154 | global api 155 | NativeExports = api.enumerate_exports(module) 156 | variables = [] 157 | functions = [] 158 | result = [] 159 | key = 1 160 | for NativeExport in NativeExports: 161 | NativeExport['key'] = str(key) 162 | NativeExport['module'] = module # save module name 163 | if(NativeExport['type'] == "variable"): 164 | variables.append(NativeExport) 165 | elif(NativeExport['type'] == "function"): 166 | functions.append(NativeExport) 167 | else: 168 | continue 169 | key = key + 1 170 | result = { 171 | 'variables': variables, 172 | 'functions': functions, 173 | } 174 | return { "errCode": 0, "result": result } 175 | 176 | # trace by list 177 | def _traceList(listString): 178 | global api 179 | listJSON = json.loads(listString) 180 | if ('java' in listJSON): 181 | if( api.is_java_available == False ): # check Java VM 182 | return {"errCode": 2, "errMsg": "The current process doesn't have a Java VM loaded."} 183 | javaMethods = listJSON['java'] 184 | if (len(javaMethods) > 0): 185 | for javaMethod in listJSON['java']: 186 | print(javaMethod) 187 | if (listJSON['intercept'] == False): 188 | if (javaMethod['parse']): # TODO (planned) using Gson/FastJson/Jackson to parse java object 189 | api.trace_method_with_parse(javaMethod['class'], javaMethod['method']) 190 | else: 191 | api.trace_method(javaMethod['class'], javaMethod['method']) 192 | else: 193 | global SERVER_PROCESS 194 | SERVER_PROCESS = multiprocessing.Process(target=start) 195 | SERVER_PROCESS.start() 196 | api.intercept_method(javaMethod['class'], javaMethod['method']) 197 | if ('native' in listJSON): 198 | nativeFunctions = listJSON['native'] 199 | if (len(nativeFunctions) > 0): 200 | for nativeFunction in listJSON['native']: 201 | print(nativeFunction) 202 | api.trace_function(nativeFunction['module'], nativeFunction['function']) 203 | return {"errCode": 0, "errMsg": "Script Loaded."} 204 | 205 | # trace native function by list 206 | def _traceByAddress(baseAddr): 207 | global api 208 | api.trace_function_by_address(baseAddr) 209 | return {"errCode": 0, "errMsg": "Script Loaded."} 210 | 211 | # websocket 212 | from channels.generic.websocket import JsonWebsocketConsumer 213 | from channels.exceptions import StopConsumer 214 | class ChatConsumer(JsonWebsocketConsumer): 215 | def websocket_connect(self, message): 216 | self.accept() 217 | global consumer 218 | consumer = self 219 | def websocket_receive(self, message): 220 | print(message) 221 | msg = message['text'] 222 | self.send(msg) 223 | def websocket_send(consumer, packet): 224 | print(packet) 225 | consumer.send_json(packet) 226 | def websocket_disconnect(self, message): 227 | # 服务端触发异常 StopConsumer 228 | raise StopConsumer 229 | 230 | -------------------------------------------------------------------------------- /backend/server/functions/frida_ps.py: -------------------------------------------------------------------------------- 1 | import json 2 | from server.functions import utils 3 | 4 | def getAppListFromUSB(): 5 | command = 'frida-ps -Uai' 6 | lines = utils.shell_execution(command) 7 | 8 | try: 9 | # command execution parse 10 | if ("PID" in lines[0]): 11 | lines = lines[2:] 12 | if ("PID" in lines[1]): 13 | lines = lines[3:] 14 | except Exception: 15 | # frida-server connection error 16 | return {"errCode":1, "errMsg":lines[0]} 17 | 18 | # serialize process list 19 | processInfos = list() 20 | key = 1 # counter, as unique mark of one process 21 | for line in lines: 22 | processInfo = line.split() 23 | processInfo_list = [ 24 | ('key', str(key)), 25 | ('pid', processInfo[0]), 26 | ('name', ' '.join(processInfo[1:-1])), # blank space may exists in process/app name 27 | ('identifier', processInfo[-1]) 28 | ] 29 | processInfos.append(dict(processInfo_list)) # list[dict] ~= json 30 | key = key + 1 # counter, as unique mark of one process 31 | 32 | return {"errCode":0, "result":processInfos} 33 | 34 | def getAppListFromLAN(): 35 | # To do 36 | return null -------------------------------------------------------------------------------- /backend/server/functions/frida_scripts/agent.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: hluwa 3 | * HomePage: https://github.com/hluwa 4 | * CreatedTime: 2020/1/7 20:44 5 | * */ 6 | 7 | 8 | var enable_deep_search = false; 9 | 10 | function verify_by_maps(dexptr, mapsptr) { 11 | var maps_offset = dexptr.add(0x34).readUInt(); 12 | var maps_size = mapsptr.readUInt(); 13 | for (var i = 0; i < maps_size; i++) { 14 | var item_type = mapsptr.add(4 + i * 0xC).readU16(); 15 | if (item_type === 4096) { 16 | var map_offset = mapsptr.add(4 + i * 0xC + 8).readUInt(); 17 | if (maps_offset === map_offset) { 18 | return true; 19 | } 20 | } 21 | } 22 | return false; 23 | } 24 | 25 | function verify(dexptr, range, enable_verify_maps) { 26 | 27 | if (range != null) { 28 | var range_end = range.base.add(range.size); 29 | // verify header_size 30 | if (dexptr.add(0x70) > range_end) { 31 | return false; 32 | } 33 | 34 | // verify file_size 35 | var dex_size = dexptr.add(0x20).readUInt(); 36 | if (dexptr.add(dex_size) > range_end) { 37 | return false; 38 | } 39 | 40 | if (enable_verify_maps) { 41 | var maps_offset = dexptr.add(0x34).readUInt(); 42 | if (maps_offset === 0) { 43 | return false 44 | } 45 | 46 | var maps_address = dexptr.add(maps_offset); 47 | if (maps_address > range_end) { 48 | return false 49 | } 50 | 51 | var maps_size = maps_address.readUInt(); 52 | if (maps_size < 2 || maps_size > 50) { 53 | return false 54 | } 55 | var maps_end = maps_address.add(maps_size * 0xC + 4); 56 | if (maps_end < range.base || maps_end > range_end) { 57 | return false 58 | } 59 | return verify_by_maps(dexptr, maps_address) 60 | } else { 61 | return dexptr.add(0x3C).readUInt() === 0x70; 62 | } 63 | } 64 | 65 | 66 | } 67 | 68 | rpc.exports = { 69 | memorydump: function memorydump(address, size) { 70 | return new NativePointer(address).readByteArray(size); 71 | }, 72 | switchmode: function switchmode(bool){ 73 | enable_deep_search = bool; 74 | }, 75 | scandex: function scandex() { 76 | var result = []; 77 | Process.enumerateRanges('r--').forEach(function (range) { 78 | try { 79 | Memory.scanSync(range.base, range.size, "64 65 78 0a 30 ?? ?? 00").forEach(function (match) { 80 | 81 | if (range.file && range.file.path 82 | && (// range.file.path.startsWith("/data/app/") || 83 | range.file.path.startsWith("/data/dalvik-cache/") || 84 | range.file.path.startsWith("/system/"))) { 85 | return; 86 | } 87 | 88 | if (verify(match.address, range, false)) { 89 | var dex_size = match.address.add(0x20).readUInt(); 90 | result.push({ 91 | "addr": match.address, 92 | "size": dex_size 93 | }); 94 | } 95 | }); 96 | 97 | if (enable_deep_search) { 98 | Memory.scanSync(range.base, range.size, "70 00 00 00").forEach(function (match) { 99 | var dex_base = match.address.sub(0x3C); 100 | if (dex_base < range.base) { 101 | return 102 | } 103 | if (dex_base.readCString(4) != "dex\n" && verify(dex_base, range, true)) { 104 | var dex_size = dex_base.add(0x20).readUInt(); 105 | result.push({ 106 | "addr": dex_base, 107 | "size": dex_size 108 | }); 109 | } 110 | }) 111 | } else { 112 | if (range.base.readCString(4) != "dex\n" && verify(range.base, range, true)) { 113 | var dex_size = range.base.add(0x20).readUInt(); 114 | result.push({ 115 | "addr": range.base, 116 | "size": dex_size 117 | }); 118 | } 119 | } 120 | 121 | } catch (e) { 122 | } 123 | }); 124 | 125 | return result; 126 | } 127 | }; -------------------------------------------------------------------------------- /backend/server/functions/frida_scripts/custom.js: -------------------------------------------------------------------------------- 1 | // You can write your own script here -------------------------------------------------------------------------------- /backend/server/functions/frida_scripts/dexDump.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Author: guoqiangck 5 | * Origin: https://github.com/GuoQiang1993/Frida-Apk-Unpack 6 | * Create: 2019/6/11 7 | * Dump dex file for packaged apk 8 | * Hook art/runtime/dex_file.cc OpenMemory or OpenCommon 9 | * Support Version: Android 4.4 and later versions 10 | */ 11 | 12 | function LogPrint(log) { 13 | var theDate = new Date(); 14 | var hour = theDate.getHours(); 15 | var minute = theDate.getMinutes(); 16 | var second = theDate.getSeconds(); 17 | var mSecond = theDate.getMilliseconds() 18 | 19 | hour < 10 ? hour = "0" + hour : hour; 20 | minute < 10 ? minute = "0" + minute : minute; 21 | second < 10 ? second = "0" + second : second; 22 | mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond; 23 | 24 | var time = hour + ":" + minute + ":" + second + ":" + mSecond; 25 | console.log("[" + time + "] " + log); 26 | } 27 | 28 | function getAndroidVersion(){ 29 | var version = 0; 30 | 31 | if(Java.available){ 32 | var versionStr = Java.androidVersion; 33 | version = versionStr.slice(0,1); 34 | }else{ 35 | LogPrint("Error: cannot get android version"); 36 | } 37 | LogPrint("Android Version: " + version); 38 | return version; 39 | } 40 | 41 | function getFunctionName(){ 42 | var i = 0; 43 | var functionName = ""; 44 | 45 | // Android 4: hook dvmDexFileOpenPartial 46 | // Android 5: hook OpenMemory 47 | // after Android 5: hook OpenCommon 48 | if(getAndroidVersion() > 4){ // android 5 and later version 49 | var artExports = Module.enumerateExportsSync("libart.so"); 50 | for(i = 0; i< artExports.length; i++){ 51 | if(artExports[i].name.indexOf("OpenMemory") !== -1){ 52 | functionName = artExports[i].name; 53 | LogPrint("index " + i + " function name: "+ functionName); 54 | break; 55 | }else if(artExports[i].name.indexOf("OpenCommon") !== -1){ 56 | functionName = artExports[i].name; 57 | LogPrint("index " + i + " function name: "+ functionName); 58 | break; 59 | } 60 | } 61 | }else{ //android 4 62 | var dvmExports = Module.enumerateExportsSync("libdvm.so"); 63 | if(dvmExports.length !== 0){ // check libdvm.so first 64 | for(i = 0; i< dvmExports.length; i++){ 65 | if(dvmExports[i].name.indexOf("dexFileParse") !== -1){ 66 | functionName = dvmExports[i].name; 67 | LogPrint("index " + i + " function name: "+ functionName); 68 | break; 69 | } 70 | } 71 | }else{ // if not load libdvm.so, check libart.so 72 | dvmExports = Module.enumerateExportsSync("libart.so"); 73 | for(i = 0; i< dvmExports.length; i++){ 74 | if(dvmExports[i].name.indexOf("OpenMemory") !== -1){ 75 | functionName = dvmExports[i].name; 76 | LogPrint("index " + i + " function name: "+ functionName); 77 | break; 78 | } 79 | } 80 | } 81 | } 82 | return functionName; 83 | } 84 | 85 | function getProcessName(){ 86 | var processName = ""; 87 | 88 | var fopenPtr = Module.findExportByName("libc.so", "fopen"); 89 | var fopenFunc = new NativeFunction(fopenPtr, 'pointer', ['pointer', 'pointer']); 90 | var fgetsPtr = Module.findExportByName("libc.so", "fgets"); 91 | var fgetsFunc = new NativeFunction(fgetsPtr, 'int', ['pointer', 'int', 'pointer']); 92 | var fclosePtr = Module.findExportByName("libc.so", "fclose"); 93 | var fcloseFunc = new NativeFunction(fclosePtr, 'int', ['pointer']); 94 | 95 | var pathPtr = Memory.allocUtf8String("/proc/self/cmdline"); 96 | var openFlagsPtr = Memory.allocUtf8String("r"); 97 | 98 | var fp = fopenFunc(pathPtr, openFlagsPtr); 99 | if(fp.isNull() === false){ 100 | var buffData = Memory.alloc(128); 101 | var ret = fgetsFunc(buffData, 128, fp); 102 | if(ret !== 0){ 103 | processName = Memory.readCString(buffData); 104 | LogPrint("processName " + processName); 105 | } 106 | fcloseFunc(fp); 107 | } 108 | return processName; 109 | } 110 | 111 | function arraybuffer2hexstr(buffer) 112 | { 113 | var hexArr = Array.prototype.map.call( 114 | new Uint8Array(buffer), 115 | function (bit) { 116 | return ('00' + bit.toString(16)).slice(-2) 117 | } 118 | ); 119 | return hexArr.join(' '); 120 | } 121 | 122 | function checkDexMagic(dataAddr){ 123 | var magicMatch = true; 124 | var magicFlagHex = [0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00]; 125 | 126 | for(var i = 0; i < 8; i++){ 127 | if(Memory.readU8(ptr(dataAddr).add(i)) !== magicFlagHex[i]){ 128 | magicMatch = false; 129 | break; 130 | } 131 | } 132 | 133 | return magicMatch; 134 | } 135 | 136 | function checkOdexMagic(dataAddr){ 137 | var magicMatch = true; 138 | var magicFlagHex = [0x64, 0x65, 0x79, 0x0a, 0x30, 0x33, 0x36, 0x00]; 139 | 140 | for(var i = 0; i < 8; i++){ 141 | if(Memory.readU8(ptr(dataAddr).add(i)) !== magicFlagHex[i]){ 142 | magicMatch = false; 143 | break; 144 | } 145 | } 146 | 147 | return magicMatch; 148 | } 149 | 150 | function dumpDex(moduleFuncName, processName){ 151 | if(moduleFuncName !== ""){ 152 | var hookFunction; 153 | if(getAndroidVersion() > 4){ // android 5 and later version 154 | hookFunction = Module.findExportByName("libart.so", moduleFuncName); 155 | }else{ // android 4 156 | hookFunction = Module.findExportByName("libdvm.so", moduleFuncName); // check libdvm.so first 157 | if(hookFunction == null) { 158 | hookFunction = Module.findExportByName("libart.so", moduleFuncName); //// if not load libdvm.so, check libart.so 159 | } 160 | } 161 | Interceptor.attach(hookFunction,{ 162 | onEnter: function(args){ 163 | var begin = 0; 164 | var dexMagicMatch = false; 165 | var odexMagicMatch = false; 166 | 167 | dexMagicMatch = checkDexMagic(args[0]); 168 | if(dexMagicMatch === true){ 169 | begin = args[0]; 170 | }else{ 171 | odexMagicMatch = checkOdexMagic(args[0]); 172 | if(odexMagicMatch === true){ 173 | begin = args[0]; 174 | } 175 | } 176 | 177 | if(begin === 0){ 178 | dexMagicMatch = checkDexMagic(args[1]); 179 | if(dexMagicMatch === true){ 180 | begin = args[1]; 181 | }else{ 182 | odexMagicMatch = checkOdexMagic(args[1]); 183 | if(odexMagicMatch === true){ 184 | begin = args[1]; 185 | } 186 | } 187 | } 188 | 189 | if(dexMagicMatch === true){ 190 | LogPrint("magic : " + Memory.readUtf8String(begin)); 191 | //console.log(hexdump(begin, { offset: 0, header: false, length: 64, ansi: false })); 192 | var address = parseInt(begin,16) + 0x20; 193 | var dex_size = Memory.readInt(ptr(address)); 194 | LogPrint("dex_size :" + dex_size); 195 | var dex_path = "/data/data/" + processName + "/dump_" + dex_size + ".dex"; // modified for Frider 196 | var dex_file = new File(dex_path, "wb"); 197 | dex_file.write(Memory.readByteArray(begin, dex_size)); 198 | dex_file.flush(); 199 | dex_file.close(); 200 | LogPrint("dump dex success, saved path: " + dex_path + "\n"); 201 | }else if(odexMagicMatch === true){ 202 | LogPrint("magic : " + Memory.readUtf8String(begin)); 203 | //console.log(hexdump(begin, { offset: 0, header: false, length: 64, ansi: false })); 204 | var address = parseInt(begin,16) + 0x0C; 205 | var odex_size = Memory.readInt(ptr(address)); 206 | LogPrint("odex_size :" + odex_size); 207 | var odex_path = "/data/data/" + processName + "/" + odex_size + ".odex"; 208 | var odex_file = new File(odex_path, "wb"); 209 | odex_file.write(Memory.readByteArray(begin, odex_size)); 210 | odex_file.flush(); 211 | odex_file.close(); 212 | LogPrint("dump odex success, saved path: " + odex_path + "\n"); 213 | } 214 | }, 215 | onLeave: function(retval){ 216 | } 217 | }); 218 | }else{ 219 | LogPrint("Error: cannot find correct module function."); 220 | } 221 | } 222 | 223 | //start dump dex file 224 | var moduleFucntionName = getFunctionName(); 225 | var processName = getProcessName(); 226 | if(moduleFucntionName !== "" && processName !== ""){ 227 | dumpDex(moduleFucntionName, processName); 228 | } -------------------------------------------------------------------------------- /backend/server/functions/frida_scripts/fridaAPI.js: -------------------------------------------------------------------------------- 1 | function _isJavaAvailable() { 2 | return Java.available 3 | } 4 | 5 | function _enumerateClasses() { 6 | const classes = null; 7 | Java.perform(function () { 8 | classes = Java.enumerateLoadedClassesSync() 9 | }); 10 | return classes 11 | } 12 | 13 | function _enumerateMethods(query) { 14 | const methods = null; 15 | Java.perform(function () { 16 | methods = Java.enumerateMethods(query) 17 | }); 18 | return methods 19 | } 20 | 21 | function _enumerateModules() { 22 | return Process.enumerateModules(); 23 | } 24 | 25 | function _enumerateExports(soName) { 26 | //return Module.enumerateSymbols(soName); 27 | return Module.enumerateExports(soName); 28 | } 29 | 30 | function _enumerateExports(soName) { 31 | // return Module.enumerateSymbols(soName); 32 | // return Module.enumerateImports(soName); 33 | return Module.enumerateExports(soName); 34 | } 35 | 36 | function forwardData(api, data){ 37 | var packet = { 38 | 'cmd': 'proxy', 39 | 'data': [api, data] 40 | }; 41 | send("Frider:::" + JSON.stringify(packet)); 42 | var retval = null; 43 | var op = recv('burp', function(value){ 44 | retval = value.data; 45 | }); 46 | op.wait(); 47 | return JSON.parse(retval); 48 | } 49 | 50 | /** 51 | * From here 52 | * modified from https://github.com/hluwa/ZenTracer/blob/master/trace.js 53 | * */ 54 | function getTid() { 55 | var Thread = Java.use("java.lang.Thread") 56 | return Thread.currentThread().getId(); 57 | } 58 | 59 | function getTName() { 60 | var Thread = Java.use("java.lang.Thread") 61 | return Thread.currentThread().getName(); 62 | } 63 | 64 | function getStack(){ 65 | return (Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); 66 | } 67 | 68 | function log(text) { 69 | var packet = { 70 | 'cmd': 'log', 71 | 'data': text 72 | }; 73 | send("Frider:::" + JSON.stringify(packet)) 74 | } 75 | 76 | function enter(tid, tname, cls, method, args) { 77 | var packet = { 78 | 'cmd': 'enter', 79 | 'data': [tid, tname, cls, method, args] 80 | }; 81 | send("Frider:::" + JSON.stringify(packet)); 82 | } 83 | 84 | function stack(tid, tname, cls, method, stack) { 85 | var packet = { 86 | 'cmd': 'stack', 87 | 'data': [tid, tname, cls, method, stack] 88 | }; 89 | send("Frider:::" + JSON.stringify(packet)) 90 | } 91 | 92 | function exit(tid, tname, cls, method, retval) { 93 | var packet = { 94 | 'cmd': 'exit', 95 | 'data': [tid, tname, cls, method, retval] 96 | }; 97 | send("Frider:::" + JSON.stringify(packet)) 98 | } 99 | 100 | function _traceMethod(className, methodNamePlus){ 101 | if (methodNamePlus.indexOf('(') !== -1){ // remove signature 102 | var methodName = methodNamePlus.split('(')[0] 103 | }else{ 104 | var methodName = methodNamePlus 105 | } 106 | Java.perform(function () { 107 | const targetClass = Java.use(className); // get class 108 | const overloads = targetClass[methodName].overloads; // get all overloads 109 | overloads.forEach(function (overload){ 110 | overload.implementation = function(){ 111 | const args = []; 112 | const tid = getTid(); 113 | const tName = getTName(); 114 | for (var i=0; i process".format(pid)) 47 | 48 | def dump(pkg_name, api, mds=None): 49 | if mds is None: 50 | mds = [] 51 | matches = api.scandex() 52 | for info in matches: 53 | try: 54 | bs = api.memorydump(info['addr'], info['size']) 55 | md = md5(bs) 56 | if md in mds: 57 | click.secho("[DEXDump]: Skip duplicate dex {}<{}>".format(info['addr'], md), fg="blue") 58 | continue 59 | mds.append(md) 60 | if not os.path.exists("./server/output/" + pkg_name + "/"): 61 | os.mkdir("./server/output/" + pkg_name + "/") 62 | if bs[:4] != b"dex\n": 63 | bs = b"dex\n035\x00" + bs[8:] 64 | with open("./server/output/" + pkg_name + "/" + info['addr'] + ".dex", 'wb') as out: 65 | out.write(bs) 66 | click.secho("[DEXDump]: DexSize={}, DexMd5={}, SavePath={}/server/output/{}/{}.dex" 67 | .format(hex(info['size']), md, os.getcwd(), pkg_name, info['addr']), fg='green') 68 | except Exception as e: 69 | click.secho("[Except] - {}: {}".format(e, info), bg='yellow') 70 | 71 | def _appUnpack(selectedApp, plan): 72 | scriptDir = './server/functions/frida_scripts/' 73 | package = json.loads(selectedApp)['package'] 74 | pid = json.loads(selectedApp)['pid'] 75 | if (plan == "Frida-Apk-Unpack"): 76 | scriptPath = scriptDir + 'dexDump.js' 77 | with open(scriptPath, 'r', encoding='utf-8') as f: 78 | scriptCode = f.read() 79 | # Unpack 80 | try: 81 | device = frida_main.get_usb_device() 82 | pid = device.spawn([package]) 83 | session = device.attach(pid) 84 | script = session.create_script(scriptCode) 85 | script.load() 86 | device.resume(pid) 87 | except Exception as ex: 88 | # script execution failed 89 | return { "errCode": 1, "errMsg": str(ex) } 90 | 91 | try: 92 | # Pull /data/data/[processName]/[dex_size].dex to /backend/server/output/ on PC 93 | command = "adb root && adb pull /data/data/{}/ ./server/output/".format(package,) 94 | lines = utils.shell_execution(command) 95 | if ("adbd cannot run as root" in lines[0]): # if adb root failed, try following: 96 | # if doesn't exist, make program folder 97 | command1 = "adb shell \"su -c 'mkdir /sdcard/frider;'\"" 98 | # if doesn't exist, make app folder 99 | command2 = "adb shell \"su -c 'cd /sdcard/frider;mkdir {};'\"".format(package,) 100 | # copy to sdcard 101 | command3 = "adb shell \"su -c 'cp /data/data/{}/dump_* /sdcard/frider/{};'\"".format(package, package) 102 | # pull dex on sdcard to local folder 103 | command4 = "adb pull /sdcard/frider/{} ./server/output/".format(package,) 104 | utils.shell_execution(command1) 105 | utils.shell_execution(command2) 106 | utils.shell_execution(command3) 107 | utils.shell_execution(command4) 108 | return { "errCode": 0 } 109 | except Exception as ex: 110 | # ERROR: copy to PC failed 111 | return { "errCode": 2, "errMsg": str(ex) } 112 | 113 | elif (plan == "FRIDA-DEXDump"): 114 | scriptPath = scriptDir + 'agent.js' 115 | try: 116 | device = frida_main.get_usb_device() 117 | except Exception as ex: 118 | # ERROR: script execution failed 119 | return { "errCode": 1, "errMsg": str(ex) } 120 | 121 | try: 122 | pid, pname = choose(device=device) 123 | except Exception as e: 124 | string = "[Except] - Unable to inject into process: {} in \n{}".format(e, traceback.format_tb(sys.exc_info()[2])[-1]) 125 | click.secho(string, bg='red') 126 | # ERROR: script execution failed 127 | return { "errCode": 1, "errMsg": string } 128 | 129 | processes = get_all_process(device, package) 130 | if (processes == []): 131 | # ERROR: Target APP is not running 132 | return { "errCode": 3, "errMsg": "Target Process Not Found" } 133 | mds = [] 134 | for process in processes: 135 | click.secho("[DEXDump]: found target [{}] {}".format(process.pid, process.name), bg='green') 136 | stop_other(process.pid, processes) 137 | with open(scriptPath, 'r', encoding='utf-8') as f: 138 | scriptCode = f.read() 139 | try: 140 | session = device.attach(process.pid) 141 | except Exception as ex: 142 | # ERROR: script execution failed 143 | return { "errCode": 1, "errMsg": str(ex) } 144 | script = session.create_script(scriptCode) 145 | script.load() 146 | dump(package, script.exports, mds=mds) 147 | script.unload() 148 | session.detach() 149 | return { "errCode": 0 } 150 | 151 | else: 152 | scriptPath = scriptDir + 'custom.js' 153 | # ERROR: Todo 154 | return { "errCode": 4, "errMsg": "Unfinished feature" } 155 | 156 | -------------------------------------------------------------------------------- /backend/server/functions/utils.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import threading 3 | 4 | encoding = 'GBK' # encode (GBK/utf-8) of your shell/cmd 5 | 6 | def shell_execution(command): 7 | pipe= subprocess.Popen(command,shell=True,stdout=subprocess.PIPE) 8 | lines = pipe.stdout.readlines() 9 | # bytes to str, with encoding 10 | counter = 0 11 | for line in lines: 12 | lines[counter] = str(line, encoding) 13 | counter = counter + 1 14 | return lines 15 | 16 | -------------------------------------------------------------------------------- /backend/server/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/backend/server/migrations/__init__.py -------------------------------------------------------------------------------- /backend/server/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /backend/server/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/server/views.py: -------------------------------------------------------------------------------- 1 | # _*_ coding:utf-8 _*_ 2 | from django.shortcuts import render 3 | from django.http import HttpResponse 4 | import json 5 | 6 | # dev 7 | from server.functions import frida_ps 8 | from server.functions import adb_parse 9 | from server.functions import frida_unpack 10 | from server.functions import frida_main 11 | 12 | def getAppList(request): 13 | data = request.POST 14 | # print(data) 15 | if data['remote'] == '1': 16 | # To do 17 | res = frida_ps.getAppListFromLAN(data['address']) 18 | else: 19 | res = frida_ps.getAppListFromUSB() 20 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 21 | 22 | def getAppInfo(request): 23 | data = request.POST 24 | res = adb_parse._getAppInfo(data['package']) 25 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 26 | 27 | def appUnpack(request): 28 | data = request.POST 29 | res = frida_unpack._appUnpack(data['selectedApp'], data['plan']) 30 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 31 | 32 | def injectApp(request): 33 | data = request.POST 34 | res = frida_main._injectApp(data['package']) 35 | # if errCode = 0, save 'injectedApp' in frontend at the same time 36 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 37 | 38 | def enumJavaClasses(request): # frontend TODO 39 | data = request.POST 40 | # 在前端检查是否已经有注入完成的APP 41 | res = frida_main._enumJavaClasses() 42 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 43 | 44 | def enumJavaMethods(request): 45 | data = request.POST 46 | # 在前端合成query: "class!method" 47 | res = frida_main._enumJavaMethods(data['query']) 48 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 49 | 50 | def enumNativeModules(request): 51 | data = request.POST 52 | res = frida_main._enumNativeModules() 53 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 54 | 55 | def enumNativeExports(request): 56 | data = request.POST 57 | res = frida_main._enumNativeExports(data['module']) 58 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 59 | 60 | def traceList(request): 61 | data = request.POST 62 | res = frida_main._traceList(data['list']) 63 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 64 | 65 | def traceByAddress(request): 66 | data = request.POST 67 | res = frida_main._traceByAddress(data['baseAddr']) 68 | return HttpResponse(json.dumps(res), content_type="application/json,charset=utf-8") 69 | 70 | -------------------------------------------------------------------------------- /backend/server/ws.py: -------------------------------------------------------------------------------- 1 | # websocket 2 | from channels.generic.websocket import WebsocketConsumer 3 | from channels.exceptions import StopConsumer 4 | 5 | class ChatConsumer(WebsocketConsumer): 6 | def websocket_connect(self, message): 7 | self.accept() 8 | def websocket_receive(self, message): 9 | print(message) 10 | msg = message['text'] 11 | self.send(msg) 12 | def websocket_send(self, packet): 13 | # print(packet) 14 | self.send(text_data=str(packet)) 15 | def websocket_disconnect(self, message): 16 | # 服务端触发异常 StopConsumer 17 | raise StopConsumer -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "./", 6 | "dependencies": { 7 | "@ant-design/icons": "^4.2.2", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.3.2", 10 | "@testing-library/user-event": "^7.1.2", 11 | "antd": "^4.4.2", 12 | "axios": "^0.19.2", 13 | "qs": "^6.9.4", 14 | "react": "^16.13.1", 15 | "react-dom": "^16.13.1", 16 | "react-highlight-words": "^0.16.0", 17 | "react-loadable": "^5.5.0", 18 | "react-scripts": "3.4.1", 19 | "react-websocket": "^2.1.0" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "react-router-dom": "^5.2.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notilus67/frider/e025d6037e02c5f44726f1d002b9f18e8c9e3ffd/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | /* .App-customMain{ 2 | position: fixed; 3 | } */ 4 | 5 | .App-contentMain{ 6 | padding: 26px 40px 0px 40px; 7 | background-color: white; 8 | /* height: 100%; */ 9 | margin-left: 200px; 10 | } 11 | 12 | .App-logo { 13 | height: 40vmin; 14 | pointer-events: none; 15 | } 16 | 17 | @media (prefers-reduced-motion: no-preference) { 18 | .App-logo { 19 | animation: App-logo-spin infinite 20s linear; 20 | } 21 | } 22 | 23 | .App-header { 24 | background-color: #282c34; 25 | min-height: 100vh; 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | justify-content: center; 30 | font-size: calc(10px + 2vmin); 31 | color: white; 32 | } 33 | 34 | .App-link { 35 | color: #61dafb; 36 | } 37 | 38 | @keyframes App-logo-spin { 39 | from { 40 | transform: rotate(0deg); 41 | } 42 | to { 43 | transform: rotate(360deg); 44 | } 45 | } -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import {BrowserRouter as Router} from 'react-router-dom'; 4 | import 'antd/dist/antd.css'; 5 | import {Layout } from 'antd'; 6 | import SiderMenu from './views/siderMenu'; 7 | import ContentMain from './Router/router'; 8 | 9 | const { 10 | Sider, Content, 11 | } = Layout; 12 | //let screenHeight= window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; 13 | 14 | class App extends React.Component { 15 | render() { 16 | return ( 17 |
18 | {/*** Important Tag ***/} 19 | 20 | {/* ? */} 21 | 22 | 23 | 24 | 25 | {/* */} 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | ); 34 | } 35 | } 36 | export default App; 37 | -------------------------------------------------------------------------------- /frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /frontend/src/Router/config.js: -------------------------------------------------------------------------------- 1 | import Loadable from 'react-loadable'; 2 | import Loading from '../loading/loader'; 3 | 4 | global.Home = Loadable( 5 | { 6 | loader: () => import('../views/home'), 7 | loading: Loading 8 | } 9 | ); 10 | 11 | global.AppInfo = Loadable( 12 | { 13 | loader: () => import('../views/appInfo'), 14 | loading: Loading 15 | } 16 | ); 17 | 18 | global.AppUnpack = Loadable( 19 | { 20 | loader: () => import('../views/appUnpack'), 21 | loading: Loading 22 | } 23 | ); 24 | 25 | global.JavaEnum = Loadable( 26 | { 27 | loader: () => import('../views/javaEnum'), 28 | loading: Loading 29 | } 30 | ); 31 | 32 | global.JavaNativeTrace = Loadable( 33 | { 34 | loader: () => import('../views/javaNativeTrace'), 35 | loading: Loading 36 | } 37 | ); 38 | 39 | global.JavaTODO = Loadable( 40 | { 41 | loader: () => import('../views/javaTODO'), 42 | loading: Loading 43 | } 44 | ); 45 | 46 | global.NativeEnum = Loadable( 47 | { 48 | loader: () => import('../views/nativeEnum'), 49 | loading: Loading 50 | } 51 | ); 52 | 53 | global.NativeTODO = Loadable( 54 | { 55 | loader: () => import('../views/nativeTODO'), 56 | loading: Loading 57 | } 58 | ); 59 | -------------------------------------------------------------------------------- /frontend/src/Router/router.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route, Switch} from 'react-router-dom'; 3 | import './config' 4 | 5 | class ContentMain extends React.Component { 6 | render() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | ) 21 | } 22 | } 23 | export default ContentMain; 24 | 25 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | height: 32px; 3 | background: rgba(255, 255, 255, 0.2); 4 | margin: 16px; 5 | } 6 | .site-layout .site-layout-background { 7 | background: #fff; 8 | } 9 | [data-theme="dark"] .site-layout .site-layout-background { 10 | background: #141414; 11 | } 12 | .siderMenu-sider{ 13 | position: fixed; 14 | background-color: #001529; 15 | } 16 | h2.ant-typography{ 17 | font-size: 20px !important; 18 | margin-top: 1.0em !important; 19 | } 20 | .stack-show{ 21 | white-space: pre-line; 22 | } -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | // 9 | , 10 | // , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /frontend/src/loading/loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {Spin, Alert} from 'antd'; 3 | 4 | class Loading extends React.Component { 5 | render() { 6 | return ( 7 |
8 | 9 | 14 | 15 |
16 | ) 17 | } 18 | } 19 | 20 | export default Loading; -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /frontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /frontend/src/views/appInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Descriptions, message } from 'antd'; 5 | import axios from 'axios'; 6 | import Qs from 'qs' 7 | 8 | const mockData = { 9 | package: "com.mock.data", 10 | versionName: 'null', 11 | versionCode: 'null', 12 | sdk: 'minSdk=null', 13 | flags: "flags=null", 14 | privateFlags: "privateFlags=null", 15 | lastUpdateTime: "null", 16 | primaryCpuAbi: "primaryCpuAbi=null", 17 | secondaryCpuAbi: "secondaryCpuAbi=null", 18 | instructionSet: "Instruction Set:null", 19 | path: "path:null", 20 | status: "status:null", 21 | codePath: "codePath=null", 22 | resourcePath: "resourcePath=null", 23 | legacyNativeLibraryDir: "legacyNativeLibraryDir=null", 24 | dataDir: "dataDir=null", 25 | usesOptionalLibraries: "usesOptionalLibraries null", 26 | usesLibraryFiles: "usesLibraryFiles null", 27 | declaredPermissions: "declared permissions: null\n", 28 | requestedPermissions: "requested permissions: null\n", 29 | }; 30 | 31 | class AppInfo extends React.Component { 32 | state = { 33 | data: mockData, 34 | }; 35 | componentDidMount(){ 36 | var selectedApp = JSON.parse(sessionStorage.getItem('selectedApp')); 37 | if (selectedApp === null) { return } 38 | const hide = message.loading('Request in progress..', 0); 39 | axios.post('http://127.0.0.1:8000/api/getAppInfo/', 40 | Qs.stringify({ 41 | 'package': selectedApp['package'], 42 | 'pid': selectedApp['pid'] 43 | }), 44 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 45 | ).then((response)=>{ 46 | hide() 47 | console.log(response) 48 | if(response.status === 200){ 49 | if (response.data['errCode'] === 0){ 50 | message.success('success') 51 | console.log('getAppInfo success') 52 | this.setState({ 53 | data: response.data['result'] 54 | }) 55 | }else{ 56 | message.error(response.data['errMsg'], 10) 57 | console.log('getAppInfo fail') 58 | console.log(response.data['errMsg']) 59 | } 60 | }else{ 61 | message.error('server error') 62 | console.log('server error') 63 | } 64 | }).catch((response)=>{ 65 | console.log(response) 66 | }) 67 | } 68 | render() { 69 | return ( 70 |
71 | 72 | {this.state.data.package} 73 | {this.state.data.versionName} ({this.state.data.versionCode}) 74 | {this.state.data.sdk} 75 | 76 | {this.state.data.flags}
77 | {this.state.data.privateFlags} 78 |
79 | {this.state.data.lastUpdateTime} 80 | 81 | {this.state.data.primaryCpuAbi}
82 | {this.state.data.secondaryCpuAbi}
83 | {this.state.data.instructionSet} 84 |
85 | 86 | {this.state.data.path}
87 | {this.state.data.status}
88 | {this.state.data.codePath}
89 | {this.state.data.resourcePath}
90 | {this.state.data.legacyNativeLibraryDir}
91 | {this.state.data.dataDir}
92 | {this.state.data.usesLibraryFiles}
93 | {this.state.data.usesOptionalLibraries} 94 |
95 | 96 | {this.state.data.declaredPermissions} 97 |
98 | {this.state.data.requestedPermissions} 99 |
100 |
, 101 |
102 | ); 103 | } 104 | } 105 | 106 | export default AppInfo; 107 | -------------------------------------------------------------------------------- /frontend/src/views/appUnpack.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Typography,message, Card, Select, Button, Switch } from 'antd'; 5 | import { Row,} from 'antd'; 6 | import axios from 'axios'; 7 | import Qs from 'qs' 8 | 9 | const { Title, Paragraph, Text } = Typography; 10 | const { Option } = Select; 11 | 12 | class AppUnpack extends React.Component { 13 | state = { 14 | package: "null", 15 | selectedApp: "null", 16 | selectedPlan: "Frida-Apk-Unpack", 17 | isChinese: false, 18 | isEnglish: true, 19 | }; 20 | 21 | componentDidMount(){ 22 | var selectedApp = JSON.parse(sessionStorage.getItem('selectedApp')); 23 | this.setState({ 24 | selectedApp: selectedApp, 25 | package: selectedApp['package'], 26 | }) 27 | }; 28 | 29 | onUnpack(){ 30 | const hide = message.loading('Action in progress..', 0); 31 | axios.post('http://127.0.0.1:8000/api/appUnpack/', 32 | Qs.stringify({ 33 | 'selectedApp': JSON.stringify(this.state.selectedApp), 34 | 'plan': this.state.selectedPlan, 35 | }), 36 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 37 | ).then((response)=>{ 38 | hide() 39 | if(response.status === 200){ 40 | console.log(response.data) 41 | if (response.data['errCode'] === 0){ 42 | message.success('Unpack success') 43 | message.success('Check output folder') 44 | console.log('Unpack success'); 45 | }else if (response.data['errCode'] === 1){ 46 | // message.error('Script execution failed') 47 | message.error(response.data['errMsg'], 10) 48 | console.log('Script execution failed or timeout') 49 | console.log(response.data['errMsg']) 50 | }else if (response.data['errCode'] === 2){ 51 | message.error('Copy to PC failed') 52 | message.error(response.data['errMsg'], 10) 53 | console.log('Copy to PC failed') 54 | console.log(response.data['errMsg']) 55 | }else if (response.data['errCode'] === 3){ 56 | message.error('Target APP is not running', 10) 57 | message.error(response.data['errMsg'], 10) 58 | console.log('Target APP is not running') 59 | console.log(response.data['errMsg']) 60 | }else if (response.data['errCode'] === 4){ 61 | message.error(response.data['errMsg'], 10) 62 | console.log(response.data['errMsg']) 63 | }else{ 64 | // placeholder 65 | } 66 | }else{ 67 | message.error('server error') 68 | console.log('server error') 69 | } 70 | }).catch((response)=>{ 71 | console.log(response) 72 | }) 73 | }; 74 | 75 | handleChange = (value) => { 76 | console.log(`selected ${value}`); 77 | this.setState({ 78 | selectedPlan: value, 79 | }) 80 | }; 81 | 82 | onTranslate(){ 83 | this.setState({ 84 | isChinese: !this.state.isChinese, 85 | isEnglish: !this.state.isEnglish, 86 | }) 87 | }; 88 | 89 | render() { 90 | return ( 91 |
92 | 93 | Unpack App 94 | Dump unpacked Android dex files to ./backend/server/output/ 95 | Your current chosen package: {this.state.package} 96 | Choose an unpack plan: 97 | 101 | 102 | 103 | 104 | 105 | More} style={{ width: 300, marginRight: 10 }}> 106 |

Authur: GuoQiang1993

107 |

Android 4.4 and higher

108 |

Hook art/runtime/dex_file.cc OpenMemory or OpenCommon

109 |
110 | More} style={{ width: 300, marginRight: 10 }}> 111 |

Authur: hluwa

112 |

All Android version supported by Frida

113 |

Fuzzy search on memory

114 |

Support muti-processes application

115 |

APP must be running on the surface

116 |
117 | 118 |

TODO

119 |
120 |
121 |
122 | Tips: 123 | 124 | { this.state.isEnglish? ( 125 |
    126 |
  • 127 | Frida-Apk-Unpack will save dump files to /data/data/(packageName) first, then copy them to PC directory by adb pull.
    128 | If you can't execute adb root, it will also save files to /sdcard/Frider/(packageName) for transit.
    129 | These directories won't be cleaned automatically. 130 |
  • 131 |
  • 132 | Running Frida-Apk-Unpack first and leaving the app on the surface may help for running FRIDA-DEXDump successfully. 133 |
  • 134 |
  • 135 | It's recommended to check output folder even if you meet timeout error when running FRIDA-DEXDump. 136 |
  • 137 |
138 | ): null } 139 | { this.state.isChinese? ( 140 |
    141 |
  • 142 | Frida-Apk-Unpack会先保存在/data/data/(packageName),然后通过adb pull发到PC上;
    143 | 如无法成功执行adb root,将会使用/sdcard/Frider/(packageName)进行中转。
    144 | 这些目录(暂时)不会自动清理。 145 |
  • 146 |
  • 147 | 在跑 FRIDA-DEXDump 之前先跑一遍 Frida-Apk-Unpack 可能会有助于绕过 ptrace 占用。 148 |
  • 149 |
  • 150 | 如果跑 FRIDA-DEXDump 遇到 timeout 报错,请依然检查 output 目录是否有 dex 文件。 151 |
  • 152 |
153 | ): null} 154 |
155 | 156 | 中文翻译: 157 | {this.onTranslate()}} /> 158 | 159 |
160 |
161 | ); 162 | } 163 | } 164 | 165 | export default AppUnpack; 166 | -------------------------------------------------------------------------------- /frontend/src/views/home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Typography, Button, Tooltip, Table, Input, Space, message } from 'antd'; 5 | import { Row, Col } from 'antd'; 6 | import Highlighter from 'react-highlight-words'; 7 | import { SearchOutlined } from '@ant-design/icons'; 8 | import axios from 'axios'; 9 | import Qs from 'qs' 10 | 11 | const { Title, Paragraph, Text } = Typography; 12 | 13 | const message_usb = ( 14 |
15 | adb forward tcp:27042 tcp:27042
16 | adb forward tcp:27043 tcp:27043
17 | ./frida-server 18 |
19 | ) 20 | 21 | const message_lan = ( 22 |
23 | ./frida-server -l 0.0.0.0 24 |
25 | ) 26 | 27 | const message_lan2 = ( 28 |
29 | Input phone IP address and the port number running frida-server 30 |
31 | ) 32 | 33 | // const mockData = [ 34 | // { 35 | // key: "1", 36 | // pid: '233', 37 | // name: "I'm mock data", 38 | // identifier: 'com.mock.data', 39 | // }, 40 | // ]; 41 | 42 | class Home extends React.Component { 43 | state = { 44 | searchText: '', 45 | searchedColumn: '', 46 | data: null, 47 | }; 48 | 49 | componentDidMount(){ 50 | var appList = JSON.parse(sessionStorage.getItem('appList')); 51 | if (appList === null) { return }; 52 | var selectedApp_string = sessionStorage.getItem('selectedApp') 53 | if (selectedApp_string !== null){ 54 | this.setState({ 55 | data: appList, 56 | selectedRowKeys : JSON.parse(selectedApp_string)['key'], 57 | }); 58 | } else { 59 | this.setState({ 60 | data: appList, 61 | }); 62 | } 63 | }; 64 | 65 | onConnect = (remote, address) => { 66 | const hide = message.loading('Action in progress..', 0); 67 | axios.post('http://127.0.0.1:8000/api/getAppList/', 68 | Qs.stringify({ 69 | 'remote': remote, 70 | 'address': address 71 | }), 72 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 73 | ).then((response)=>{ 74 | hide() 75 | if(response.status === 200){ 76 | console.log(response.data) 77 | if (response.data['errCode'] === 0){ 78 | message.success('connect success') 79 | console.log('getAppList success'); 80 | this.setState({ 81 | data: response.data['result'] 82 | }); 83 | sessionStorage.setItem('appList', JSON.stringify(response.data['result'])); 84 | }else{ 85 | message.error(response.data['errMsg'], 10) 86 | console.log('connect fail') 87 | console.log(response.data['errMsg']) 88 | } 89 | 90 | }else{ 91 | message.error('server error') 92 | console.log('server error') 93 | } 94 | }).catch((response)=>{ 95 | console.log(response) 96 | }) 97 | } 98 | 99 | getColumnSearchProps = dataIndex => ({ 100 | filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => ( 101 |
102 | { 104 | this.searchInput = node; 105 | }} 106 | placeholder={`Search ${dataIndex}`} 107 | value={selectedKeys[0]} 108 | onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} 109 | onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)} 110 | style={{ width: 188, marginBottom: 8, display: 'block' }} 111 | /> 112 | 113 | 122 | 125 | 126 |
127 | ), 128 | filterIcon: filtered => , 129 | onFilter: (value, record) => 130 | record[dataIndex] ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()) : '', 131 | onFilterDropdownVisibleChange: visible => { 132 | if (visible) { 133 | setTimeout(() => this.searchInput.select()); 134 | } 135 | }, 136 | render: text => 137 | this.state.searchedColumn === dataIndex ? ( 138 | 144 | ) : ( 145 | text 146 | ), 147 | }); 148 | 149 | handleSearch = (selectedKeys, confirm, dataIndex) => { 150 | confirm(); 151 | this.setState({ 152 | searchText: selectedKeys[0], 153 | searchedColumn: dataIndex, 154 | }); 155 | }; 156 | 157 | handleReset = clearFilters => { 158 | clearFilters(); 159 | this.setState({ searchText: '' }); 160 | }; 161 | 162 | render() { 163 | const columns = [ 164 | { 165 | title: 'Pid', 166 | dataIndex: 'pid', 167 | key: 'pid', 168 | ...this.getColumnSearchProps('pid'), 169 | }, 170 | { 171 | title: 'Name', 172 | dataIndex: 'name', 173 | key: 'name', 174 | ...this.getColumnSearchProps('name'), 175 | 176 | }, 177 | { 178 | title: 'Identifier', 179 | dataIndex: 'identifier', 180 | key: 'identifier', 181 | ...this.getColumnSearchProps('identifier'), 182 | 183 | }, 184 | // { 185 | // title: 'Action', 186 | // key: 'select', 187 | // render: text => Select, 188 | // }, 189 | ]; 190 | const rowSelection = { 191 | onChange: (selectedRowKeys, selectedRows) => { 192 | // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); 193 | this.setState({ 194 | selectedRowKeys : selectedRowKeys, 195 | }); 196 | // save selected APP 197 | var selectedApp = JSON.stringify({ 198 | package: selectedRows[0]['identifier'], 199 | pid: selectedRows[0]['pid'], 200 | key: selectedRowKeys[0], 201 | }) 202 | console.log("selectedApp:" + selectedApp) 203 | sessionStorage.setItem('selectedApp', selectedApp) 204 | }, 205 | getCheckboxProps: record => ({ 206 | disabled: record.name === 'Disabled User', 207 | // Column configuration not to be checked 208 | name: record.name, 209 | }), 210 | }; 211 | 212 | return ( 213 |
214 | 215 | Start frida-server 216 | 217 | Before pressing Connect, you should check adb connectivity and either: 218 | 219 | 220 |
    221 |
  • 222 | 223 | 224 | 225 | run frida-server in USB mode 226 | 227 | 228 | 229 | 230 | 231 | 232 |
  • 233 |
  • 234 | 235 | 236 | run frida-server in LAN mode (TODO) 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 |
  • 250 |
251 |
252 | 262 | 263 | 264 | ); 265 | } 266 | } 267 | 268 | export default Home; 269 | -------------------------------------------------------------------------------- /frontend/src/views/javaEnum.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Typography, Button, Tooltip, Table, Input, Space, message, Checkbox, Tree } from 'antd'; 5 | import { Row, Col } from 'antd'; 6 | import Highlighter from 'react-highlight-words'; 7 | import { SearchOutlined } from '@ant-design/icons'; 8 | import axios from 'axios'; 9 | import Qs from 'qs' 10 | 11 | const { Title, Paragraph } = Typography; 12 | 13 | const message_inject = ( 14 |
15 | Inject frida scripts to target APP
16 |
17 | ) 18 | 19 | const message_classFilter = ( 20 |
21 | className filter, default: *
22 |
23 | ) 24 | 25 | const message_methodFilter = ( 26 |
27 | methodName filter, cannot be empty
28 |
29 | ) 30 | 31 | // const mockClassData = [ 32 | // { 33 | // key: '1', 34 | // class: 'com.mock.data', 35 | // }, 36 | // ]; 37 | 38 | // const treeData0 = [ 39 | // { 40 | // title: 'loader: null', 41 | // key: '0-0', 42 | // children: [ 43 | // { 44 | // title: 'class 1', 45 | // key: '0-0-0', 46 | // children: [ 47 | // { 48 | // title: 'method 1', 49 | // key: '0-0-0-0', 50 | // }, 51 | // { 52 | // title: 'method 2', 53 | // key: '0-0-0-1', 54 | // }, 55 | // ], 56 | // }, 57 | // { 58 | // title: 'class 2', 59 | // key: '0-0-1', 60 | // children: [ 61 | // { 62 | // title: 'method 3', 63 | // key: '0-0-1-0', 64 | // }, 65 | // ], 66 | // }, 67 | // ], 68 | // }, 69 | // ]; 70 | 71 | class JavaEnum extends React.Component { 72 | state = { 73 | searchText: '', 74 | searchedColumn: '', 75 | classData: null, 76 | classFilter: '*', 77 | methodFilter: '', 78 | checkedValues: ['i', 's'], 79 | }; 80 | 81 | componentDidMount(){ 82 | var selectedApp = JSON.parse(sessionStorage.getItem('selectedApp')); 83 | var injectedApp = sessionStorage.getItem('injectedApp'); 84 | this.setState({ 85 | selectedApp: selectedApp, 86 | package: selectedApp['package'], 87 | injectedApp: injectedApp, 88 | }); 89 | if (injectedApp != null){ 90 | this.onEnumClasses(); 91 | } 92 | }; 93 | 94 | onInject() { 95 | const hide = message.loading('Action in progress..', 0); 96 | axios.post('http://127.0.0.1:8000/api/injectApp/', 97 | Qs.stringify({ 98 | 'package': this.state.package, 99 | }), 100 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 101 | ).then((response)=>{ 102 | hide() 103 | if(response.status === 200){ 104 | console.log(response.data) 105 | if (response.data['errCode'] === 0){ 106 | message.success('inject success') 107 | console.log('inject app(package) success'); 108 | this.setState({ 109 | injectedApp: this.state.package 110 | }) 111 | sessionStorage.setItem('injectedApp', this.state.injectedApp); 112 | this.onEnumClasses(); // auto enumerate java classes after injection 113 | }else{ 114 | message.error(response.data['errMsg'], 10) 115 | console.log('inject fail') 116 | console.log(response.data['errMsg']) 117 | } 118 | }else{ 119 | message.error('server error') 120 | console.log('server error') 121 | } 122 | }).catch((response)=>{ 123 | console.log(response) 124 | }) 125 | }; 126 | 127 | onEnumClasses() { 128 | const hide = message.loading('Action in progress..', 0); 129 | axios.post('http://127.0.0.1:8000/api/enumJavaClasses/', 130 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 131 | ).then((response)=>{ 132 | hide() 133 | if(response.status === 200){ 134 | console.log(response.data) 135 | if (response.data['errCode'] === 0){ 136 | message.success('enumerate classes success') 137 | console.log('enumerate Java classes success'); 138 | this.setState({ 139 | classData: response.data['result'] 140 | }) 141 | }else{ 142 | message.error(response.data['errMsg'], 10) 143 | console.log('enumerate fail') 144 | console.log(response.data['errMsg']) 145 | } 146 | }else{ 147 | message.error('server error') 148 | console.log('server error') 149 | } 150 | }).catch((response)=>{ 151 | console.log(response) 152 | }) 153 | }; 154 | 155 | onEnumMethods= query => { 156 | console.log("method query: " , query); 157 | const hide = message.loading('Action in progress..', 0); 158 | axios.post('http://127.0.0.1:8000/api/enumJavaMethods/', 159 | Qs.stringify({ 160 | 'query': query, 161 | }), 162 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 163 | ).then((response)=>{ 164 | hide() 165 | if(response.status === 200){ 166 | console.log(response.data) 167 | if (response.data['errCode'] === 0){ 168 | message.success('enumerate methods success') 169 | console.log('enumerate Java methods success'); 170 | this.setState({ 171 | treeData: this.processMethodData(response.data['result']), 172 | }) 173 | }else{ 174 | message.error(response.data['errMsg'], 10) 175 | console.log('enumerate fail') 176 | console.log(response.data['errMsg']) 177 | } 178 | }else{ 179 | message.error('server error') 180 | console.log('server error') 181 | } 182 | }).catch((response)=>{ 183 | console.log(response) 184 | }) 185 | }; 186 | 187 | // processMethodData is to be improved 188 | processMethodData = methodData => { 189 | const nodeList = [] 190 | const loaderNum = methodData.length 191 | for(let i=0;i ({ 236 | filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => ( 237 |
238 | { 240 | this.searchInput = node; 241 | }} 242 | placeholder={`Search ${dataIndex}`} 243 | value={selectedKeys[0]} 244 | onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} 245 | onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)} 246 | style={{ width: 188, marginBottom: 8, display: 'block' }} 247 | /> 248 | 249 | 258 | 261 | 262 |
263 | ), 264 | filterIcon: filtered => , 265 | onFilter: (value, record) => 266 | record[dataIndex] ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()) : '', 267 | onFilterDropdownVisibleChange: visible => { 268 | if (visible) { 269 | setTimeout(() => this.searchInput.select()); 270 | } 271 | }, 272 | render: text => 273 | this.state.searchedColumn === dataIndex ? ( 274 | 280 | ) : ( 281 | text 282 | ), 283 | }); 284 | 285 | handleSearch = (selectedKeys, confirm, dataIndex) => { 286 | confirm(); 287 | this.setState({ 288 | searchText: selectedKeys[0], 289 | searchedColumn: dataIndex, 290 | }); 291 | }; 292 | 293 | handleReset = clearFilters => { 294 | clearFilters(); 295 | this.setState({ searchText: '' }); 296 | }; 297 | 298 | onCheckboxChange = (checkedValues) => { 299 | console.log('checkedBox = ', checkedValues); 300 | }; 301 | 302 | onSelectMethod = (selectedKeys, ev) => { 303 | // console.log('selected', selectedKeys, ev); 304 | // add new item to traceList 305 | if (ev.node.hasOwnProperty('p_title')){ 306 | var traceList = JSON.parse(sessionStorage.getItem('traceList')); 307 | var itemString = ev.node['p_title'] + "|||" + ev.node['title'] + "|||" + 'java'; 308 | if (traceList == null){ 309 | traceList = [] 310 | traceList.push(itemString); 311 | console.log(traceList) 312 | message.success('added to traceList') 313 | }else{ 314 | if (traceList.indexOf(itemString) === -1){ // no repeat 315 | traceList.push(itemString); 316 | message.success('added to traceList'); 317 | message.info('already in the list'); 318 | } 319 | } 320 | sessionStorage.setItem('traceList', JSON.stringify(traceList)); 321 | } 322 | }; 323 | 324 | render() { 325 | const columns = [ 326 | { 327 | title: 'Class', 328 | dataIndex: 'class', 329 | key: 'class', 330 | ...this.getColumnSearchProps('class'), 331 | }, 332 | { 333 | title: 'Action', 334 | key: 'select', 335 | render: (text, record) => {this.onEnumMethods(record.class+'*!*/is')}}>Enumerate Methods, 336 | }, 337 | ]; 338 | return ( 339 |
340 | 341 | Enumerate Java Class/Method 342 | 343 | 344 | Current selected package: {this.state.package}. To inject/reinject, please press: 345 | 346 | 347 | 348 | 349 | 350 | 351 | Current injected package: {this.state.injectedApp}. 352 | 353 | Plan A: Find method by class 354 |
359 | Plan B: Find method by filter 360 | 361 | {this.setState({checkedValues: checkedValues})}} defaultValue={'is'}> 362 | Case-insensitive 363 | Include method signatures 364 | User-defined classes only 365 | 366 | 367 | 368 | 369 | 370 | 371 | {this.setState({classFilter: ev.target.value})}}/> 372 | 373 | 374 | 375 | 376 | {this.setState({methodFilter: ev.target.value})}}/> 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | Method List (click method to join trace list) 385 | { this.state.treeData? ( // wait for data, otherwise defaultExpandAll won't take effect 386 | 391 | ): null } 392 | 393 | 394 | ); 395 | } 396 | } 397 | 398 | export default JavaEnum; 399 | -------------------------------------------------------------------------------- /frontend/src/views/javaNativeTrace.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Typography, Button, Tooltip, Table, Space, message, Switch, Popconfirm, Timeline, Input } from 'antd'; 5 | import { Row, Col } from 'antd'; 6 | import axios from 'axios'; 7 | import Qs from 'qs' 8 | import Websocket from 'react-websocket'; 9 | 10 | const { Title, Paragraph, Text } = Typography; 11 | 12 | const message_inject = ( 13 |
14 | Inject frida scripts to target APP
15 |
16 | ) 17 | 18 | const message_intercept = ( 19 |
20 | redirect to 127.0.0.1:8080
21 | use BurpSuite(etc.) to intercept data
22 |
23 | ) 24 | 25 | const message_addr = ( 26 |
27 | base address of a library
28 | can be found on NativeEnumerate page
29 |
30 | ) 31 | 32 | 33 | class JavaNativeTrace extends React.Component { 34 | state = { 35 | searchText: '', 36 | searchedColumn: '', 37 | traceList: null, 38 | onWebsocket: false, 39 | showStack: true, 40 | enableIntercept: false, 41 | }; 42 | 43 | componentDidMount(){ 44 | var selectedApp = JSON.parse(sessionStorage.getItem('selectedApp')); 45 | var injectedApp = sessionStorage.getItem('injectedApp'); 46 | var traceList = JSON.parse(sessionStorage.getItem('traceList')); 47 | this.setState({ 48 | selectedApp: selectedApp, 49 | package: selectedApp['package'], 50 | injectedApp: injectedApp, 51 | traceList: this.formatTraceListToShow(traceList), 52 | }); 53 | }; 54 | 55 | formatTraceListToShow= traceList => { 56 | // add 'key' to traceItems 57 | var formatTraceList = []; 58 | if(traceList !== null){ 59 | for(let i=0;i { 74 | // add 'key' to traceItems 75 | var javaList = []; 76 | var nativeList = []; 77 | var result = null; 78 | if(traceList !== null){ 79 | for(let i=0;i{ 114 | hide() 115 | if(response.status === 200){ 116 | console.log(response.data) 117 | if (response.data['errCode'] === 0){ 118 | message.success('inject success') 119 | console.log('inject app(package) success'); 120 | this.setState({ 121 | injectedApp: this.state.package 122 | }) 123 | sessionStorage.setItem('injectedApp', this.state.injectedApp); 124 | // this.onEnumClasses(); // auto enumerate java classes after injection 125 | }else{ 126 | message.error(response.data['errMsg'], 10) 127 | console.log('inject fail') 128 | console.log(response.data['errMsg']) 129 | } 130 | }else{ 131 | message.error('server error') 132 | console.log('server error') 133 | } 134 | }).catch((response)=>{ 135 | console.log(response) 136 | }) 137 | }; 138 | 139 | handleDelete = key => { 140 | const dataSource = [...this.state.traceList]; 141 | this.setState({ 142 | traceList: dataSource.filter(item => item.key !== key) 143 | }); 144 | // delete sessionStorage 145 | const deleteItem = dataSource[key]; 146 | const deleteString = deleteItem.class + "|||" + deleteItem.method + "|||" + deleteItem.type; 147 | var storageTraceList = JSON.parse(sessionStorage.getItem('traceList')); 148 | var index = storageTraceList.indexOf(deleteString); 149 | if (index > -1){ 150 | storageTraceList.splice(index,1); 151 | } 152 | sessionStorage.setItem('traceList', JSON.stringify(storageTraceList)); 153 | }; 154 | 155 | pushTimeline = packet => { 156 | var traceLine = this.state.traceLine; // get existed timeline 157 | if (traceLine === null || traceLine === undefined){ 158 | traceLine = []; 159 | } 160 | packet = JSON.parse(packet); 161 | const elementTitle = packet.data[2] + "::" + packet.data[3]; // classname::methodname 162 | if (packet.cmd === 'enter'){ 163 | const args = packet.data[4]; 164 | traceLine.push( 165 |

{elementTitle}

166 | {args.map((arg, index) => {return

arg[{index}]: {args[index]}

})} 167 |
); 168 | }; 169 | if (packet.cmd === 'exit'){ 170 | const retval = packet.data[4]; 171 | traceLine.push( 172 |

{elementTitle}

173 |

retval: {retval}

174 |
); 175 | }; 176 | if (packet.cmd === 'stack' && this.state.showStack){ 177 | const stack = packet.data[4]; 178 | traceLine.push( 179 |

{elementTitle}

180 | {stack} 181 |
); 182 | }; 183 | this.setState({ 184 | traceLine: traceLine, 185 | }); 186 | }; 187 | 188 | onTraceByList(){ 189 | const hide = message.loading('Action in progress..', 0); 190 | axios.post('http://127.0.0.1:8000/api/traceList/', 191 | Qs.stringify({ 192 | 'list': this.formatTraceListToSend(this.state.traceList), 193 | }), 194 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 195 | ).then((response)=>{ 196 | hide(); 197 | if(response.status === 200){ 198 | console.log(response.data) 199 | if (response.data['errCode'] === 0){ 200 | message.success('trace added') 201 | console.log('trace added'); 202 | this.setState({ 203 | onWebsocket: true, 204 | }); 205 | }else{ 206 | message.error(response.data['errMsg'], 10) 207 | console.log('trace fail') 208 | console.log(response.data['errMsg']) 209 | } 210 | }else{ 211 | message.error('server error') 212 | console.log('server error') 213 | } 214 | }).catch((response)=>{ 215 | console.log(response) 216 | }) 217 | }; 218 | 219 | onTraceByAddress(){ 220 | if(this.state.baseAddr !== null){ 221 | const hide = message.loading('Action in progress..', 0); 222 | axios.post('http://127.0.0.1:8000/api/traceByAddress/', 223 | Qs.stringify({ 224 | 'baseAddr': this.state.baseAddr, 225 | }), 226 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 227 | ).then((response)=>{ 228 | hide(); 229 | if(response.status === 200){ 230 | console.log(response.data) 231 | if (response.data['errCode'] === 0){ 232 | message.success('trace added') 233 | console.log('trace added'); 234 | this.setState({ 235 | onWebsocket: true, 236 | }); 237 | }else{ 238 | message.error(response.data['errMsg'], 10) 239 | console.log('trace fail') 240 | console.log(response.data['errMsg']) 241 | } 242 | }else{ 243 | message.error('server error') 244 | console.log('server error') 245 | } 246 | }).catch((response)=>{ 247 | console.log(response) 248 | }) 249 | } 250 | }; 251 | 252 | render() { 253 | const columns = [ 254 | { 255 | title: 'Class/Module', 256 | dataIndex: 'class', 257 | key: 'class', 258 | }, 259 | { 260 | title: 'Method/Function', 261 | dataIndex: 'method', 262 | key: 'method', 263 | }, 264 | { 265 | title: 'Type', 266 | dataIndex: 'type', 267 | key: 'type', 268 | }, 269 | { 270 | title: 'Action', 271 | dataIndex: 'action', 272 | render: (text, record) => 273 | this.state.traceList.length >= 1 ? ( 274 | this.handleDelete(record.key)}> 275 | Delete 276 | 277 | ) : null, 278 | }, 279 | ]; 280 | return ( 281 |
282 | 283 | Trace Java method / Native function 284 | 285 | 286 | Current selected package: {this.state.package}. To inject/reinject, please press: 287 | 288 | 289 | 290 | 291 | 292 | 293 | Current injected package: {this.state.injectedApp}. 294 | 295 | 296 | Attention: When you want trace a native function, please manually parse arguments/retval in backend\server\functions\frida_scripts\fridaAPI.js and re-inject. 297 | 298 | 299 | 300 | Plan A: Trace by list 301 | 302 | 303 | 304 |
309 | Plan B: Trace by native address 310 | 311 | 312 | 313 | {this.setState({baseAddr: e.target.value})}} placeholder="base address" /> 314 | 315 | 316 | 317 | 318 | 319 | 320 | Tracer 321 | 322 | 323 | Show stack? {this.setState({showStack: checked})}} /> 324 | 325 | Intercept(Java) ? {this.setState({enableIntercept: checked})}} /> 326 | 327 | 328 | 329 | { this.state.traceLine? ( 330 | 331 | {this.state.traceLine} 332 | 333 | ): null } 334 | 335 | { this.state.onWebsocket? ( 336 | {this.pushTimeline(packet)}}/> 337 | ): null} 338 | 339 | ); 340 | } 341 | } 342 | 343 | export default JavaNativeTrace; 344 | -------------------------------------------------------------------------------- /frontend/src/views/javaTODO.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Typography, } from 'antd'; 5 | 6 | const { Title } = Typography; 7 | 8 | class JavaTODO extends React.Component { 9 | componentDidMount(){ 10 | 11 | }; 12 | render() { 13 | return ( 14 |
15 | 16 | TODO 17 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | export default JavaTODO; 24 | -------------------------------------------------------------------------------- /frontend/src/views/nativeEnum.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Typography, Button, Tooltip, Table, Input, Space, message,Radio, Switch } from 'antd'; 5 | import { Row } from 'antd'; 6 | import Highlighter from 'react-highlight-words'; 7 | import { SearchOutlined } from '@ant-design/icons'; 8 | import axios from 'axios'; 9 | import Qs from 'qs' 10 | 11 | const { Title, Paragraph } = Typography; 12 | 13 | const message_inject = ( 14 |
15 | Inject frida scripts to target APP
16 |
17 | ) 18 | 19 | const message_refresh = ( 20 |
21 | Refresh this page to see later-loaded modules
22 |
23 | ) 24 | 25 | const mockModuleData = [ 26 | { 27 | appOnly: [ 28 | // { 29 | // key: '1', 30 | // name: 'mock', 31 | // class: '0x1', 32 | // size: 0, 33 | // path: "/", 34 | // }, 35 | ], 36 | all: [ 37 | // { 38 | // key: '1', 39 | // name: 'mock', 40 | // class: '0x1', 41 | // size: 0, 42 | // path: "/", 43 | // }, 44 | ], 45 | }, 46 | ]; 47 | 48 | class NativeEnum extends React.Component { 49 | state = { 50 | searchText: '', 51 | searchedColumn: '', 52 | moduleData: mockModuleData, 53 | }; 54 | 55 | componentDidMount(){ 56 | var selectedApp = JSON.parse(sessionStorage.getItem('selectedApp')); 57 | var injectedApp = sessionStorage.getItem('injectedApp'); 58 | this.setState({ 59 | selectedApp: selectedApp, 60 | package: selectedApp['package'], 61 | injectedApp: injectedApp, 62 | showModuleData: this.state.moduleData['appOnly'], 63 | }); 64 | if (injectedApp != null){ 65 | this.onEnumModules(); 66 | } 67 | }; 68 | 69 | onInject() { 70 | const hide = message.loading('Action in progress..', 0); 71 | axios.post('http://127.0.0.1:8000/api/injectApp/', 72 | Qs.stringify({ 73 | 'package': this.state.package, 74 | }), 75 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 76 | ).then((response)=>{ 77 | hide() 78 | if(response.status === 200){ 79 | console.log(response.data) 80 | if (response.data['errCode'] === 0){ 81 | message.success('inject success') 82 | console.log('inject app(package) success'); 83 | this.setState({ 84 | injectedApp: this.state.package 85 | }) 86 | sessionStorage.setItem('injectedApp', this.state.injectedApp); 87 | this.onEnumModules(); // auto enumerate java classes after injection 88 | }else{ 89 | message.error(response.data['errMsg'], 10) 90 | console.log('inject fail') 91 | console.log(response.data['errMsg']) 92 | } 93 | }else{ 94 | message.error('server error') 95 | console.log('server error') 96 | } 97 | }).catch((response)=>{ 98 | console.log(response) 99 | }) 100 | }; 101 | 102 | onEnumModules() { 103 | const hide = message.loading('Action in progress..', 0); 104 | axios.post('http://127.0.0.1:8000/api/enumNativeModules/', 105 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 106 | ).then((response)=>{ 107 | hide() 108 | if(response.status === 200){ 109 | console.log(response.data) 110 | if (response.data['errCode'] === 0){ 111 | message.success('enumerate native modules success') 112 | console.log('enumerate native modules success'); 113 | this.setState({ 114 | moduleData: response.data['result'], 115 | showModuleData: response.data['result']['appOnly'], 116 | }) 117 | }else{ 118 | message.error(response.data['errMsg'], 10) 119 | console.log('enumerate fail') 120 | console.log(response.data['errMsg']) 121 | } 122 | }else{ 123 | message.error('server error') 124 | console.log('server error') 125 | } 126 | }).catch((response)=>{ 127 | console.log(response) 128 | }) 129 | }; 130 | 131 | onEnumExports= module => { 132 | const hide = message.loading('Action in progress..', 0); 133 | axios.post('http://127.0.0.1:8000/api/enumNativeExports/', 134 | Qs.stringify({ 135 | 'module': module, 136 | }), 137 | {headers:{'Content-Type':'application/x-www-form-urlencoded'}} 138 | ).then((response)=>{ 139 | hide() 140 | if(response.status === 200){ 141 | console.log(response.data) 142 | if (response.data['errCode'] === 0){ 143 | message.success('enumerate native exports success') 144 | console.log('enumerate native exports success'); 145 | this.setState({ 146 | exportData: response.data['result'], 147 | showExportData: response.data['result']['functions'] // default show functions 148 | }) 149 | }else{ 150 | message.error(response.data['errMsg'], 10) 151 | console.log('enumerate fail') 152 | console.log(response.data['errMsg']) 153 | } 154 | }else{ 155 | message.error('server error') 156 | console.log('server error') 157 | } 158 | }).catch((response)=>{ 159 | console.log(response) 160 | }) 161 | }; 162 | 163 | getColumnSearchProps = dataIndex => ({ 164 | filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => ( 165 |
166 | { 168 | this.searchInput = node; 169 | }} 170 | placeholder={`Search ${dataIndex}`} 171 | value={selectedKeys[0]} 172 | onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} 173 | onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)} 174 | style={{ width: 188, marginBottom: 8, display: 'block' }} 175 | /> 176 | 177 | 186 | 189 | 190 |
191 | ), 192 | filterIcon: filtered => , 193 | onFilter: (value, record) => 194 | record[dataIndex] ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()) : '', 195 | onFilterDropdownVisibleChange: visible => { 196 | if (visible) { 197 | setTimeout(() => this.searchInput.select()); 198 | } 199 | }, 200 | render: text => 201 | this.state.searchedColumn === dataIndex ? ( 202 | 208 | ) : ( 209 | text 210 | ), 211 | }); 212 | 213 | handleSearch = (selectedKeys, confirm, dataIndex) => { 214 | confirm(); 215 | this.setState({ 216 | searchText: selectedKeys[0], 217 | searchedColumn: dataIndex, 218 | }); 219 | }; 220 | 221 | handleReset = clearFilters => { 222 | clearFilters(); 223 | this.setState({ searchText: '' }); 224 | }; 225 | 226 | onSelectExport = (record) => { 227 | if (record['type'] === 'function'){ 228 | var traceList = JSON.parse(sessionStorage.getItem('traceList')); 229 | var itemString = record['module'] + "|||" + record['name'] + "|||" + "native"; 230 | if (traceList == null){ 231 | traceList = [] 232 | traceList.push(itemString); 233 | message.success('added to traceList') 234 | }else{ 235 | if (traceList.indexOf(itemString) === -1){ // no repeat 236 | traceList.push(itemString); 237 | message.success('added to traceList'); 238 | message.info('already in the list'); 239 | } 240 | } 241 | sessionStorage.setItem('traceList', JSON.stringify(traceList)); 242 | }else{ 243 | // select variable 244 | message.error('cannot trace variable') 245 | } 246 | }; 247 | 248 | render() { 249 | const moduleColumns = [ 250 | { 251 | title: 'Name', 252 | dataIndex: 'name', 253 | key: 'name', 254 | ...this.getColumnSearchProps('name'), 255 | }, 256 | { 257 | title: 'Base Address ', 258 | dataIndex: 'base', 259 | key: 'base', 260 | ...this.getColumnSearchProps('base'), 261 | }, 262 | { 263 | title: 'Size (bytes)', 264 | dataIndex: 'size', 265 | key: 'size', 266 | ...this.getColumnSearchProps('size'), 267 | }, 268 | { 269 | title: 'Path', 270 | dataIndex: 'path', 271 | key: 'path', 272 | ...this.getColumnSearchProps('path'), 273 | }, 274 | { 275 | title: 'Action', 276 | key: 'select', 277 | render: (text, record) => {this.onEnumExports(record.name)}}>Enumerate Exports, 278 | }, 279 | ]; 280 | const exportColumns = [ 281 | { 282 | title: 'Name', 283 | dataIndex: 'name', 284 | key: 'name', 285 | ...this.getColumnSearchProps('name'), 286 | }, 287 | { 288 | title: 'Address ', 289 | dataIndex: 'address', 290 | key: 'address', 291 | ...this.getColumnSearchProps('address'), 292 | }, 293 | { 294 | title: 'Type', 295 | dataIndex: 'type', 296 | key: 'type', 297 | }, 298 | { 299 | title: 'Action', 300 | key: 'select', 301 | render: (text, record) => {this.onSelectExport(record)}}>Trace, 302 | }, 303 | ]; 304 | return ( 305 |
306 | 307 | Enumerate Native Library/Export 308 | 309 | 310 | Current selected package: {this.state.package}. To inject/reinject, please press: 311 | 312 | 313 | 314 | 315 | 316 | 317 | Current injected package: {this.state.injectedApp}. 318 | 319 | 320 | <Tooltip title={message_refresh}>Find exports by module</Tooltip> 321 | 322 | 323 | 324 | Show app modules only? {this.setState({showModuleData: checked ? this.state.moduleData['appOnly'] : this.state.moduleData['all']})}} /> 325 | 326 | 327 |
332 | Exports List 333 | { this.state.exportData? ( 334 |
335 | 336 | 337 | Show: 338 | {this.setState({showExportData: ev.target.value})}}> 339 | functions 340 | variables 341 | 342 | 343 | 344 |
349 | 350 | ): null } 351 | 352 | 353 | ); 354 | } 355 | } 356 | 357 | export default NativeEnum; 358 | -------------------------------------------------------------------------------- /frontend/src/views/nativeTODO.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'antd/dist/antd.css'; 3 | import '../index.css'; 4 | import { Typography, } from 'antd'; 5 | 6 | const { Title } = Typography; 7 | 8 | class NativeTODO extends React.Component { 9 | componentDidMount(){ 10 | 11 | }; 12 | render() { 13 | return ( 14 |
15 | 16 | TODO 17 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | export default NativeTODO; 24 | -------------------------------------------------------------------------------- /frontend/src/views/siderMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { Layout, Menu } from 'antd'; 4 | import { AndroidOutlined, LaptopOutlined, HomeOutlined} from '@ant-design/icons'; 5 | import 'antd/dist/antd.css'; 6 | import '../index.css'; 7 | 8 | const { Sider } = Layout; 9 | const { SubMenu } = Menu; 10 | 11 | class SiderMenu extends React.Component { 12 | render() { 13 | return ( 14 | 15 | 16 |
17 | 18 | }> 19 | Home 20 | 21 | } title="APP"> 22 | 23 | AppInfo 24 | 25 | 26 | AppUnpack 27 | 28 | 29 | } title="JAVA"> 30 | 31 | Enumerate 32 | 33 | 34 | Trace 35 | 36 | 37 | TODO 38 | 39 | 40 | } title="NATIVE"> 41 | 42 | Enumerate 43 | 44 | 45 | Trace 46 | 47 | 48 | TODO 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | } 57 | 58 | //ReactDOM.render(, document.getElementById('root')); 59 | 60 | export default SiderMenu; --------------------------------------------------------------------------------