From 6e875fb77baa9dbe65ff8e7d0344d740e1d6d51e Mon Sep 17 00:00:00 2001 From: Ajin Abraham Date: Mon, 23 Mar 2026 01:46:03 -0700 Subject: [PATCH] Jailbroken iOS Device Support (#2536) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables dynamic analysis on real jailbroken iOS devices connected via USB or WiFi SSH, complementing the existing Corellium-based iOS analysis. - SSH connectivity — connects to jailbroken devices over USB (via iproxy port forwarding) or WiFi using Paramiko SSH - Frida instrumentation — spawns/attaches Frida on-device, with full hook support matching the existing Corellium flow - Environment setup — installs AppSync Unified and Frida server on first run; supports both arm and arm64 devices - Dynamic analysis — app file extraction, system log streaming (oslog), screenshot capture, process listing, and report generation - SSH terminal — execute shell commands on the device over SSH --------- Co-authored-by: CylentSec Co-authored-by: Oz --- .../android/default/debugger_check_bypass.js | 2 +- .../android/default/root_bypass.js | 4 +- .../android/others/dump-library-exports.js | 2 +- .../android/others/helper-early-java.js | 5 +- .../android/others/helper-trace-jni.js | 4 +- .../android/others/ssl-pinning-bypass.js | 2 +- .../android/others/stop-app-terminate.js | 2 +- .../android/others/trace-file.js | 4 +- .../ios/auxiliary/class-trace.js | 119 +- .../ios/auxiliary/find-app-classes-methods.js | 114 +- .../ios/auxiliary/find-app-classes.js | 60 +- .../ios/auxiliary/find-specific-method.js | 115 +- .../ios/auxiliary/get-methods.js | 84 +- .../ios/auxiliary/search-class-pattern.js | 89 +- .../ios/auxiliary/string-capture.js | 41 +- .../ios/auxiliary/string-compare.js | 77 +- .../tools/frida_scripts/ios/dump/crypto.js | 24 +- .../tools/frida_scripts/ios/dump/data-dir.js | 39 +- .../tools/frida_scripts/ios/dump/json.js | 2 +- .../tools/frida_scripts/ios/dump/keychain.js | 10 +- .../tools/frida_scripts/ios/dump/nslog.js | 4 +- .../ios/others/bypass-ssl-flutter.js | 22 +- .../ios/others/bypass-ssl-ios10.js | 23 - .../ios/others/bypass-ssl-ios11.js | 24 - .../ios/others/bypass-ssl-ios12.js | 58 - .../ios/others/bypass-ssl-ios13.js | 51 - .../ios/others/bypass-ssl-trustkit.js | 60 +- .../ios/others/detect-http-clients.js | 57 + .../ios/others/detect-network.js | 27 + .../ios/others/find-all-classes-methods.js | 87 +- .../ios/others/find-all-classes.js | 50 +- .../frida_scripts/ios/others/get-exports.js | 7 +- .../frida_scripts/ios/others/get-modules.js | 3 +- .../ios/others/trace-bluetooth.js | 2 +- .../tools/frida_scripts/ios/rpc/screenshot.js | 92 + .../frida_scripts/ios/rpc/ssl-pinning.js | 265 +++ ...5aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb | Bin 0 -> 51658 bytes ...5aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb | Bin 0 -> 49472 bytes .../ios/oslog/oslog_0.0.1-8_iphoneos-arm.deb | Bin 0 -> 8940 bytes .../oslog/oslog_0.0.4.3_iphoneos-arm64.deb | Bin 0 -> 8470 bytes .../views/android/environment.py | 13 +- .../views/android/frida_server_download.py | 80 - mobsf/DynamicAnalyzer/views/common/device.py | 21 +- .../views/common/frida/server_update.py | 128 ++ mobsf/DynamicAnalyzer/views/common/shared.py | 10 +- .../views/ios/corellium_instance.py | 8 +- .../views/ios/device/connect.py | 448 +++++ .../views/ios/device/device.py | 504 ++++++ .../views/ios/device/dynamic_analyzer.py | 508 ++++++ .../views/ios/device/environment.py | 319 ++++ .../DynamicAnalyzer/views/ios/device/frida.py | 179 ++ .../views/ios/device/report.py | 312 ++++ .../views/ios/dynamic_analyzer.py | 69 +- mobsf/DynamicAnalyzer/views/ios/frida_core.py | 34 +- mobsf/DynamicAnalyzer/views/ios/helpers.py | 296 ++++ mobsf/DynamicAnalyzer/views/ios/report.py | 9 +- mobsf/MobSF/init.py | 12 +- mobsf/MobSF/security.py | 5 + mobsf/MobSF/settings.py | 10 + mobsf/MobSF/urls.py | 77 +- mobsf/MobSF/utils.py | 16 + .../views/api/api_ios_device_analysis.py | 211 +++ mobsf/static/others/css/spinner.css | 4 +- .../android/dynamic_analyzer.html | 128 +- .../dynamic_analysis/ios/api_monitor.html | 148 ++ .../ios/device/dynamic_analysis.html | 335 ++++ .../ios/device/dynamic_analyzer.html | 1063 ++++++++++++ .../ios/device/dynamic_report.html | 1485 +++++++++++++++++ .../ios/device/system_logs.html | 244 +++ .../ios/dynamic_analysis.html | 2 +- .../ios/dynamic_analyzer.html | 137 +- .../dynamic_analysis/ios/dynamic_report.html | 69 +- .../dynamic_analysis/ios/system_logs.html | 2 +- mobsf/templates/general/apidocs.html | 1061 +++++++++++- mobsf/templates/general/dynamic.html | 14 +- pyproject.toml | 2 +- 76 files changed, 8669 insertions(+), 925 deletions(-) delete mode 100644 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios10.js delete mode 100644 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios11.js delete mode 100644 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios12.js delete mode 100755 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios13.js create mode 100644 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-http-clients.js create mode 100644 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-network.js create mode 100644 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/screenshot.js create mode 100644 mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/ssl-pinning.js create mode 100644 mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb create mode 100644 mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm64.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb create mode 100644 mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.1-8_iphoneos-arm.deb create mode 100644 mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.4.3_iphoneos-arm64.deb delete mode 100644 mobsf/DynamicAnalyzer/views/android/frida_server_download.py create mode 100644 mobsf/DynamicAnalyzer/views/common/frida/server_update.py create mode 100644 mobsf/DynamicAnalyzer/views/ios/device/connect.py create mode 100644 mobsf/DynamicAnalyzer/views/ios/device/device.py create mode 100644 mobsf/DynamicAnalyzer/views/ios/device/dynamic_analyzer.py create mode 100644 mobsf/DynamicAnalyzer/views/ios/device/environment.py create mode 100644 mobsf/DynamicAnalyzer/views/ios/device/frida.py create mode 100644 mobsf/DynamicAnalyzer/views/ios/device/report.py create mode 100644 mobsf/DynamicAnalyzer/views/ios/helpers.py create mode 100644 mobsf/MobSF/views/api/api_ios_device_analysis.py create mode 100644 mobsf/templates/dynamic_analysis/ios/api_monitor.html create mode 100644 mobsf/templates/dynamic_analysis/ios/device/dynamic_analysis.html create mode 100644 mobsf/templates/dynamic_analysis/ios/device/dynamic_analyzer.html create mode 100644 mobsf/templates/dynamic_analysis/ios/device/dynamic_report.html create mode 100644 mobsf/templates/dynamic_analysis/ios/device/system_logs.html diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js index e604db97..1b6f976a 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/debugger_check_bypass.js @@ -15,7 +15,7 @@ try { Interceptor.attach(libc.getExportByName("connect"), { onEnter: function(args) { try{ - var memory = Memory.readByteArray(args[1], 64); + var memory = args[1].readByteArray(64); var b = new Uint8Array(memory); if (b[2] == 0x69 && b[3] == 0xa2 && b[4] == 0x7f && b[5] == 0x00 && b[6] == 0x00 && b[7] == 0x01) { this.frida_detection = true; diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js index 6a26cf9a..54554e77 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/default/root_bypass.js @@ -211,7 +211,7 @@ Java.performNow(function () { Interceptor.attach(libc.getExportByName("fopen"), { onEnter: function (args) { try{ - var path = Memory.readCString(args[0]); + var path = args[0].readCString(); path = path.split("/"); var executable = path[path.length - 1]; var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1) @@ -229,7 +229,7 @@ Java.performNow(function () { Interceptor.attach(libc.getExportByName("system"), { onEnter: function (args) { try{ - var cmd = Memory.readCString(args[0]); + var cmd = args[0].readCString(); send("[RootDetection Bypass] SYSTEM CMD: " + cmd); if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") { send("[RootDetection Bypass] native system: " + cmd); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-library-exports.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-library-exports.js index 217f459f..f489e0fc 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-library-exports.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/dump-library-exports.js @@ -24,7 +24,7 @@ Java.perform(function () var libName = f.getName() console.log("Native lib name: " + libName) - var exports = Module.enumerateExportsSync(libName) + var exports = Process.getModuleByName(libName).enumerateExports() console.log("Exported methods:") if (exports === undefined || exports.length == 0) diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-early-java.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-early-java.js index 3971d678..217110ef 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-early-java.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-early-java.js @@ -1,10 +1,11 @@ // Source: https://github.com/apkunpacker/FridaScripts +// Updated for Frida 17.0.0+ compatibility var Duplicate = []; -Module.enumerateExportsSync("libart.so").forEach(function(exp) { +Process.getModuleByName("libart.so").enumerateExports().forEach(function(exp) { if (exp.name.indexOf("ClassLinker") != -1 && exp.name.indexOf("FindClassE") != -1) { Interceptor.attach(exp.address, { onEnter: function(args) { - this.name = Memory.readCString(args[2]); + this.name = args[2].readCString(); }, onLeave: function(retval) { if (Duplicate.indexOf(this.name) >= 0) return; diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-trace-jni.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-trace-jni.js index d1b78277..ae59e1f2 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-trace-jni.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/helper-trace-jni.js @@ -44,8 +44,8 @@ Java.perform(function () { } }); - send("[✔] Hook installed on libc open()"); + send("[+] Hook installed on libc open()"); } catch (e) { - send("[✘] Error hooking libc open: " + e); + send("[-] Error hooking libc open: " + e); } }); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js index 0b979984..5e1bb95b 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/ssl-pinning-bypass.js @@ -991,7 +991,7 @@ setTimeout(function() { return ret; }; netBuilder.addPublicKeyPins.implementation = function(hostName, pinsSha256, includeSubdomains, expirationDate) { - console.log("[+] Сronet addPublicKeyPins hostName = " + hostName); + console.log("[+] Cronet addPublicKeyPins hostName = " + hostName); return this; }; } catch (err) {} diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/stop-app-terminate.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/stop-app-terminate.js index 09310ade..f409a737 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/stop-app-terminate.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/stop-app-terminate.js @@ -168,7 +168,7 @@ try { const libc = Process.getModuleByName("libc.so"); Interceptor.attach(libc.getExportByName("system"), { onEnter: function(args) { - var cmd = Memory.readCString(args[0]); + var cmd = args[0].readCString(); if (cmd.indexOf("kill") != -1) { console.log("Bypass native system: " + cmd); var NewKill = args[0].writeUtf8String("bypassed"); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-file.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-file.js index 35cad367..5963f988 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-file.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/android/others/trace-file.js @@ -313,7 +313,7 @@ Java.perform(function () { const path = TraceSysFD["fd-" + fd] || "[unknown path]"; let data = null; try { - data = Memory.readByteArray(bfr, sz); + data = bfr.readByteArray(sz); } catch (_) { data = null; } @@ -344,7 +344,7 @@ Java.perform(function () { const path = TraceSysFD["fd-" + fd] || "[unknown path]"; let data = null; try { - data = Memory.readByteArray(bfr, sz); + data = bfr.readByteArray(sz); } catch (_) { data = null; } diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/class-trace.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/class-trace.js index ffcb1790..6c75613f 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/class-trace.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/class-trace.js @@ -1,49 +1,82 @@ -/* Description: Hook all the methods of a particular class - * Mode: S+A - * Version: 1.0 - * Credit: https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -function hook_class_method(class_name, method_name) -{ - var hook = ObjC.classes[class_name][method_name]; - Interceptor.attach(hook.implementation, { - onEnter: function(args) { - send("[AUXILIARY] Detected call to: " + class_name + " -> " + method_name); - } - }); +function hook_class_method(className, methodName) { + try { + const method = ObjC.classes[className][methodName]; + const impl = method?.implementation; + + if (!impl || impl.isNull()) { + send(`⚠️ Skipping ${methodName} — no implementation.`); + return; + } + + Interceptor.attach(impl, { + onEnter: function (args) { + send(`[AUXILIARY] Detected call to: ${className} -> ${methodName}`); + + for (let i = 0; i < method.argumentTypes.length; i++) { + try { + const arg = args[i]; + if (arg.isNull && arg.isNull()) { + send(`[AUXILIARY] arg[${i}]: null`); + } else { + try { + const str = arg.readUtf8String(); + send(`[AUXILIARY] arg[${i}]: "${str}"`); + } catch { + send(`[AUXILIARY] arg[${i}]: ${arg}`); + } + } + } catch (err) { + send(`[AUXILIARY] arg[${i}]: `); + } + } + }, + + onLeave: function (retval) { + try { + if (retval.isNull && retval.isNull()) { + send(`[AUXILIARY] return: null`); + } else { + send(`[AUXILIARY] return: ${retval}`); + } + } catch (err) { + send(`[AUXILIARY] return: `); + } + } + }); + } catch (err) { + send(`❌ Failed to hook ${className} -> ${methodName}: ${err.message}`); + } } -function run_hook_all_methods_of_specific_class(className_arg) -{ - send("Started: Hook all methods of a specific class"); - send("Class Name: " + className_arg); - //Your class name here - var className = className_arg; - //var methods = ObjC.classes[className].$methods; - var methods = ObjC.classes[className].$ownMethods; - for (var i = 0; i < methods.length; i++) - { - send("[AUXILIARY] [-] "+methods[i]); - send("[AUXILIARY] \t[*] Hooking into implementation"); - //eval('var className2 = "'+className+'"; var funcName2 = "'+methods[i]+'"; var hook = eval(\'ObjC.classes.\'+className2+\'["\'+funcName2+\'"]\'); Interceptor.attach(hook.implementation, { onEnter: function(args) { console.log("[*] Detected call to: " + className2 + " -> " + funcName2); } });'); - var className2 = className; - var funcName2 = methods[i]; - hook_class_method(className2, funcName2); - // send("[AUXILIARY] \t[*] Hooking successful"); - } - send("[AUXILIARY] Completed: Hook all methods of a specific class"); +function run_hook_all_methods_of_specific_class(className) { + send(`📦 Started hooking all methods of: ${className}`); + + try { + if (!ObjC.classes.hasOwnProperty(className)) { + send(`❌ Class not found: ${className}`); + return; + } + + const methods = ObjC.classes[className].$ownMethods; + + for (const methodName of methods) { + send(`[AUXILIARY] Hooking: ${methodName}`); + hook_class_method(className, methodName); + } + + send("✅ Completed hooking all methods."); + } catch (err) { + send(`❌ Error while hooking class: ${err.message}`); + } } -function hook_all_methods_of_specific_class(className_arg) -{ - try { - setImmediate(run_hook_all_methods_of_specific_class,[className_arg]) - } catch(err) {} +function hook_all_methods_of_specific_class(classNameArg) { + try { + setImmediate(run_hook_all_methods_of_specific_class, classNameArg); + } catch (err) { + send(`❌ Unexpected error: ${err.message}`); + } } - -//Your class name goes here -hook_all_methods_of_specific_class('{{CLASS}}') \ No newline at end of file +// Replace '{{CLASS}}' with the class name you want to hook +hook_all_methods_of_specific_class('{{CLASS}}'); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes-methods.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes-methods.js index a0b9cfed..fdc880df 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes-methods.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes-methods.js @@ -1,54 +1,68 @@ -/* Description: Dump all methods inside classes owned by the app only - * Mode: S+A - * Version: 1.0 - * Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -// Modified to support Frida 17.0.0+ +function run_show_app_classes_methods_only() { + send("Started: Find App's Classes and Methods"); -function run_show_app_classes_methods_only() -{ - send("Started: Find App's Classes and Methods") - var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer']) - var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer']) - var p = Memory.alloc(Process.pointerSize) - p.writeUInt(0) - var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String() - var pPath = Memory.allocUtf8String(path) - var pClasses = copyClassNamesForImage(pPath, p) - var count = p.readUInt() - var classesArray = new Array(count) - for (var i = 0; i < count; i++) - { - var pClassName = pClasses.add(i * Process.pointerSize).readPointer() - classesArray[i] = pClassName.readUtf8String() - var className = classesArray[i] - send("[AUXILIARY] Class: " + className); - //var methods = ObjC.classes[className].$methods; - var methods = ObjC.classes[className].$ownMethods; - for (var j = 0; j < methods.length; j++) - { - send("[AUXILIARY] \t[-] Method: " + methods[j]); - try - { - send("[AUXILIARY] \t\t[-] Arguments Type: " + ObjC.classes[className][methods[j]].argumentTypes); - send("[AUXILIARY] \t\t[-] Return Type: " + ObjC.classes[className][methods[j]].returnType); - } - catch(err) {} - } - } - free(pClasses) - send("App Classes found: " + count); - send("Completed: Find App's Classes") -} - -function show_app_classes_methods_only() -{ try { - setImmediate(run_show_app_classes_methods_only) - } catch(err) {} + const free = new NativeFunction( + Process.getModuleByName('libc++abi.dylib').getExportByName('free'), + 'void', + ['pointer'] + ); + + const copyClassNamesForImage = new NativeFunction( + Process.getModuleByName('libobjc.A.dylib').getExportByName('objc_copyClassNamesForImage'), + 'pointer', + ['pointer', 'pointer'] + ); + + const path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String(); + const pPath = Memory.allocUtf8String(path); + const pCount = Memory.alloc(Process.pointerSize); + pCount.writeUInt(0); + + const pClasses = copyClassNamesForImage(pPath, pCount); + const count = pCount.readUInt(); + + send(`[AUXILIARY] Classes found in app image: ${count}`); + + for (let i = 0; i < count; i++) { + const classPtr = pClasses.add(i * Process.pointerSize).readPointer(); + const className = classPtr.readUtf8String(); + + if (!ObjC.classes.hasOwnProperty(className)) { + continue; + } + + const klass = ObjC.classes[className]; + send(`[AUXILIARY] Class: ${className}`); + + const methods = klass.$ownMethods; + for (const methodName of methods) { + send(` [-] Method: ${methodName}`); + + try { + const method = klass[methodName]; + send(` ├─ Args: ${method.argumentTypes}`); + send(` └─ Return: ${method.returnType}`); + } catch (err) { + send(` ⚠️ Failed to inspect method: ${err.message}`); + } + } + } + + free(pClasses); + } catch (err) { + send(`❌ Error: ${err.message}`); + } + + send("Completed: Find App's Classes"); } -show_app_classes_methods_only() +function show_app_classes_methods_only() { + try { + setImmediate(run_show_app_classes_methods_only); + } catch (err) { + send(`❌ Unexpected error: ${err.message}`); + } +} + +show_app_classes_methods_only(); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes.js index 0cbdb14d..ceca8c9a 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-app-classes.js @@ -1,41 +1,29 @@ -/* Description: Dump classes owned by the app only - * Mode: S+A - * Version: 1.0 - * Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -// Modified to support Frida 17.0.0+ - -function run_show_app_classes_only() -{ - send("Started: Find App's Classes") - var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer']) - var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer']) - var p = Memory.alloc(Process.pointerSize) - p.writeUInt(0) - var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String() - var pPath = Memory.allocUtf8String(path) - var pClasses = copyClassNamesForImage(pPath, p) - var count = p.readUInt() - var classesArray = new Array(count) - for (var i = 0; i < count; i++) - { - var pClassName = pClasses.add(i * Process.pointerSize).readPointer() - classesArray[i] = pClassName.readUtf8String() - send("[AUXILIARY] " + classesArray[i]) +function listAppClasses() { + if (!ObjC.available) { + send("❌ Objective-C runtime is not available."); + return; } - free(pClasses) - send("App Classes found: " + count); - send("Completed: Find App's Classes") -} -function show_app_classes_only() -{ try { - setImmediate(run_show_app_classes_only) - } catch(err) {} + const loaded = ObjC.enumerateLoadedClassesSync(); + let count = 0; + + for (const imagePath in loaded) { + if (!imagePath.includes(".app")) continue; + + const classList = loaded[imagePath]; + send(`📦 Classes in ${imagePath}: ${classList.length}`); + + for (const className of classList) { + send(`[AUXILIARY] ${className}`); + count++; + } + } + + send(`✅ Total app-owned classes found: ${count}`); + } catch (err) { + send("❌ Error during enumeration: " + err.message); + } } -show_app_classes_only() +setImmediate(listAppClasses); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-specific-method.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-specific-method.js index 78d2c89c..44892b2d 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-specific-method.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/find-specific-method.js @@ -1,55 +1,76 @@ -/* Description: Find a specific method in all classes in the app - * Modified for MobSF - * Mode: S+A - * Version: 1.0 - * Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -// Modified to support Frida 17.0.0+ +function run_show_classes_with_method(pattern) { + send(`Started: Find App's Classes with method matching pattern '${pattern}'`); -function find_specific_method_in_all_classes(func_name) -{ - send("Searching for method [" + func_name + "] in all Classes"); - var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer']) - var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer']) - var p = Memory.alloc(Process.pointerSize) - p.writeUInt(0) - var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String() - var pPath = Memory.allocUtf8String(path) - var pClasses = copyClassNamesForImage(pPath, p) - var count = p.readUInt() - var classesArray = new Array(count) - for (var i = 0; i < count; i++) - { - var pClassName = pClasses.add(i * Process.pointerSize).readPointer() - classesArray[i] = pClassName.readUtf8String() - var className = classesArray[i] - //var methods = ObjC.classes[className].$methods; - var methods = ObjC.classes[className].$ownMethods; - for (var j = 0; j < methods.length; j++) - { - if(methods[j].includes(func_name)) - { - send("[AUXILIARY] Class: " + className); - send("[AUXILIARY] \t[-] Method: " + methods[j]); - try - { - send("[AUXILIARY] \t\t[-] Arguments Type: " + ObjC.classes[className][methods[j]].argumentTypes); - send("[AUXILIARY] \t\t[-] Return Type: " + ObjC.classes[className][methods[j]].returnType); + try { + const free = new NativeFunction( + Process.getModuleByName('libc++abi.dylib').getExportByName('free'), + 'void', + ['pointer'] + ); + + const copyClassNamesForImage = new NativeFunction( + Process.getModuleByName('libobjc.A.dylib').getExportByName('objc_copyClassNamesForImage'), + 'pointer', + ['pointer', 'pointer'] + ); + + const path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String(); + const pPath = Memory.allocUtf8String(path); + const pCount = Memory.alloc(Process.pointerSize); + pCount.writeUInt(0); + + const pClasses = copyClassNamesForImage(pPath, pCount); + const count = pCount.readUInt(); + + send(`[AUXILIARY] Classes found in app image: ${count}`); + + const regex = new RegExp(pattern); + + for (let i = 0; i < count; i++) { + const classPtr = pClasses.add(i * Process.pointerSize).readPointer(); + const className = classPtr.readUtf8String(); + + if (!ObjC.classes.hasOwnProperty(className)) { + continue; + } + + const klass = ObjC.classes[className]; + const methods = klass.$ownMethods; + + const matchedMethods = methods.filter(name => regex.test(name)); + if (matchedMethods.length === 0) continue; + + send(`[MATCHED] Class: ${className}`); + + for (const methodName of matchedMethods) { + send(` [-] Method: ${methodName}`); + + try { + const method = klass[methodName]; + send(` ├─ Args: ${method.argumentTypes}`); + send(` └─ Return: ${method.returnType}`); + } catch (err) { + send(` ⚠️ Failed to inspect method: ${err.message}`); } - catch(err) {} } } + + free(pClasses); + } catch (err) { + send(`❌ Error: ${err.message}`); } - free(pClasses) - send("Completed: Find specific Method in all Classes"); + + send("Completed: Find matching classes"); } +// Usage example: +function show_classes_with_method(pattern) { + try { + setImmediate(() => run_show_classes_with_method(pattern)); + } catch (err) { + send(`❌ Unexpected error: ${err.message}`); + } +} -//Your function name goes here -var METHOD = '{{METHOD}}' -try { - find_specific_method_in_all_classes(METHOD) -} catch(err) {} \ No newline at end of file +// Example call: +show_classes_with_method("{{METHOD}}"); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/get-methods.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/get-methods.js index cca064b1..cdabd0ab 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/get-methods.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/get-methods.js @@ -1,63 +1,29 @@ -/* Description: Get all methods of the class - * Modified for MobSF - * Mode: S+A - * Version: 1.0 - * Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -// Modified to support Frida 17.0.0+ - -function run_get_app_methods_in_class() -{ - var targetClass = '{{CLASS}}'; - var found = false; - send("Looking for methods in: " + targetClass) - - var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer']) - var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer']) - var p = Memory.alloc(Process.pointerSize) - p.writeUInt(0) - var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String() - var pPath = Memory.allocUtf8String(path) - var pClasses = copyClassNamesForImage(pPath, p) - var count = p.readUInt() - var classesArray = new Array(count) - for (var i = 0; i < count; i++) - { - var pClassName = pClasses.add(i * Process.pointerSize).readPointer() - classesArray[i] = pClassName.readUtf8String() - var className = classesArray[i] - if (className === targetClass) - { - found = true; - send("[AUXILIARY] Class: " + className); - //var methods = ObjC.classes[className].$methods; - var methods = ObjC.classes[className].$ownMethods; - for (var j = 0; j < methods.length; j++) - { - send("[AUXILIARY] \t[-] Method: " + methods[j]); - try - { - send("[AUXILIARY] \t\t[-] Arguments Type: " + ObjC.classes[className][methods[j]].argumentTypes); - send("[AUXILIARY] \t\t[-] Return Type: " + ObjC.classes[className][methods[j]].returnType); - } - catch(err) {} - } - } +function dumpMethodsOfClass(className) { + if (!ObjC.available) { + send("❌ Objective-C runtime not available"); + return; } - free(pClasses) - if (!found) - { - send("Class not found: " + targetClass) - } else - { - send("Completed Enumerating Methods in Class: " + targetClass) + + try { + const klass = ObjC.classes[className]; + if (!klass) { + send(`❌ Class not found: ${className}`); + return; + } + + send(`✅ Found class: ${className}`); + const methods = klass.$methods; // includes inherited and static methods + + if (methods.length === 0) { + send(`[AUXILIARY] ⚠️ No methods found for class ${className}`); + return; + } + + send(`[AUXILIARY] 🧬 Methods for ${className} (${methods.length} total):`); + methods.forEach(method => send(` [-] ${method}`)); + } catch (err) { + send(`❌ Error dumping methods for ${className}: ${err.message}`); } } - -try { - run_get_app_methods_in_class() -} catch(err) {} \ No newline at end of file +setImmediate(dumpMethodsOfClass, "{{CLASS}}"); \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/search-class-pattern.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/search-class-pattern.js index ca4bbaa8..78c3a4c5 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/search-class-pattern.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/search-class-pattern.js @@ -1,59 +1,50 @@ -/* Description: Find classes matching a pattern - * Modified for MobSF - * Mode: S+A - * Version: 1.0 - * Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -// Modified to support Frida 17.0.0+ +function findClasses(pattern) { + const foundClasses = []; -function findClasses(pattern) -{ - var foundClasses = []; - var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer']) - var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer']) - var p = Memory.alloc(Process.pointerSize) - p.writeUInt(0) - var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String() - var pPath = Memory.allocUtf8String(path) - var pClasses = copyClassNamesForImage(pPath, p) - var count = p.readUInt() - var classesArray = new Array(count) - for (var i = 0; i < count; i++) - { - var pClassName = pClasses.add(i * Process.pointerSize).readPointer() - classesArray[i] = pClassName.readUtf8String() - if (classesArray[i].match(pattern)) { - foundClasses.push( classesArray[i]); - } + if (!ObjC.available) { + send("❌ Objective-C runtime is not available."); + return foundClasses; } - free(pClasses) + + try { + const classMap = ObjC.enumerateLoadedClassesSync(); + + for (const imagePath in classMap) { + if (!imagePath.includes(".app")) continue; // App-only classes + + for (const className of classMap[imagePath]) { + if (pattern.test(className)) { + foundClasses.push(className); + } + } + } + } catch (err) { + send("❌ Error during class enumeration: " + err.message); + } + return foundClasses; } +function getMatches() { + try { + const pattern = /{{PATTERN}}/i; // Replace this with your actual search + send("🔍 Searching for class names matching pattern: " + pattern); -function getMatches(){ - var matches; - try{ - var pattern = /{{PATTERN}}/i; - send('Class search for pattern: ' + pattern) - matches = findClasses(pattern); - }catch (err){ - send('Class pattern match [\"Error\"] => ' + err); - return; + const matches = findClasses(pattern); + + if (matches.length > 0) { + send(`✅ Found [${matches.length}] match(es):`); + matches.forEach(className => send("[AUXILIARY] " + className)); + } else { + send("❌ No matches found."); + } + } catch (err) { + send("❌ Error during pattern match: " + err.message); } - if (matches.length>0) - send('Found [' + matches.length + '] matches') - else - send('No matches found') - matches.forEach(function(clz) { - send('[AUXILIARY] ' + clz) - }); } - try { - getMatches(); -} catch(err) {} \ No newline at end of file + setImmediate(getMatches); +} catch (err) { + send("❌ Script scheduling failed: " + err.message); +} diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-capture.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-capture.js index 6ddb379e..452fe03f 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-capture.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-capture.js @@ -1,14 +1,37 @@ function captureString() { - send('Capturing strings') - Interceptor.attach(ObjC.classes.NSString['+ stringWithUTF8String:'].implementation, { - onLeave: function (retval) { - var str = new ObjC.Object(ptr(retval)).toString() - send('[AUXILIARY] [NSString stringWithUTF8String:] -> '+ str); - return retval; + if (!ObjC.available) { + send("❌ Objective-C runtime is not available"); + return; + } + + try { + const classRef = ObjC.classes.NSString; + if (!classRef || !classRef['+ stringWithUTF8String:']) { + send("❌ NSString +stringWithUTF8String: not found"); + return; } - }); + + send("📦 Hooking NSString +stringWithUTF8String:"); + + Interceptor.attach(classRef['+ stringWithUTF8String:'].implementation, { + onLeave: function (retval) { + if (retval.isNull()) return; + + try { + const str = new ObjC.Object(retval).toString(); + send("[AUXILIARY] [NSString stringWithUTF8String:] -> " + str); + } catch (err) { + send("[AUXILIARY] ⚠️ Failed to convert NSString: " + err.message); + } + } + }); + } catch (err) { + send("❌ Error while hooking NSString: " + err.message); + } } try { - captureString(); -} catch(err) {} \ No newline at end of file + setImmediate(captureString); +} catch (err) { + send("❌ Unexpected error: " + err.message); +} diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js index 4869ba7a..08c94560 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/auxiliary/string-compare.js @@ -1,27 +1,66 @@ function captureStringCompare() { - send('Capturing string comparisons') - Interceptor.attach(ObjC.classes.__NSCFString['- isEqualToString:'].implementation, { - onEnter: function (args) { - var src = new ObjC.Object(ptr(args[0])).toString() - var str = new ObjC.Object(ptr(args[2])).toString() - send('[AUXILIARY] [__NSCFString isEqualToString:] -> \nstring 1: '+ src + '\nstring 2: '+ str); + if (!ObjC.available) { + send("❌ Objective-C runtime not available."); + return; + } + + try { + const cls = ObjC.classes.__NSCFString; + if (!cls || !cls["- isEqualToString:"]) { + send("❌ __NSCFString or method -isEqualToString: not available."); + return; } - }); + + Interceptor.attach(cls["- isEqualToString:"].implementation, { + onEnter: function (args) { + try { + const str1 = new ObjC.Object(args[0]).toString(); + const str2 = new ObjC.Object(args[2]).toString(); + send(`[AUXILIARY] __NSCFString -isEqualToString:\n ↳ string 1: ${str1}\n ↳ string 2: ${str2}`); + } catch (e) { + send(`⚠️ Failed to capture comparison: ${e.message}`); + } + } + }); + } catch (err) { + send("❌ Hooking __NSCFString failed: " + err.message); + } } -function captureStringCompare2(){ - Interceptor.attach(ObjC.classes.NSTaggedPointerString['- isEqualToString:'].implementation, { - onEnter: function (args) { - var src = new ObjC.Object(ptr(args[0])).toString() - var str = new ObjC.Object(ptr(args[2])).toString() - send('[AUXILIARY] NSTaggedPointerString[- isEqualToString:] -> \nstring 1: '+ src + '\nstring 2: '+ str); +function captureStringCompare2() { + if (!ObjC.available) return; + + try { + const cls = ObjC.classes.NSTaggedPointerString; + if (!cls || !cls["- isEqualToString:"]) { + send("❌ NSTaggedPointerString or method -isEqualToString: not available."); + return; } - }); + + Interceptor.attach(cls["- isEqualToString:"].implementation, { + onEnter: function (args) { + try { + const str1 = new ObjC.Object(args[0]).toString(); + const str2 = new ObjC.Object(args[2]).toString(); + send(`[AUXILIARY] NSTaggedPointerString -isEqualToString:\n ↳ string 1: ${str1}\n ↳ string 2: ${str2}`); + } catch (e) { + send(`⚠️ Failed to capture tagged comparison: ${e.message}`); + } + } + }); + } catch (err) { + send("❌ Hooking NSTaggedPointerString failed: " + err.message); + } } -try { - captureStringCompare(); -} catch(err) {} try { - captureStringCompare2(); -} catch(err) {} \ No newline at end of file + setImmediate(captureStringCompare); +} catch (err) { + send("❌ Error scheduling captureStringCompare: " + err.message); +} + +try { + setImmediate(captureStringCompare2); +} catch (err) { + send("❌ Error scheduling captureStringCompare2: " + err.message); +} diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/crypto.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/crypto.js index 0813f281..1f18f7d8 100755 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/crypto.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/crypto.js @@ -34,9 +34,9 @@ try { CCOperation: args[0].toInt32(), CCAlgorithm: args[1].toInt32(), CCOptions: args[2].toInt32(), - Key: !args[3].isNull() ? base64ArrayBuffer(Memory.readByteArray(args[3], args[4].toInt32())) : null, - IV: !args[5].isNull() ? base64ArrayBuffer(Memory.readByteArray(args[5], 16)) : null, - dataInput: !args[6].isNull() ? base64ArrayBuffer(Memory.readByteArray(args[6], args[7].toInt32())) : null + Key: !args[3].isNull() ? base64ArrayBuffer(args[3].readByteArray(args[4].toInt32())) : null, + IV: !args[5].isNull() ? base64ArrayBuffer(args[5].readByteArray(16)) : null, + dataInput: !args[6].isNull() ? base64ArrayBuffer(args[6].readByteArray(args[7].toInt32())) : null }; this.dataOut = args[8]; this.dataOutLength = args[10]; @@ -45,7 +45,7 @@ try { onLeave: function(retval) { const cccrypt_re = { dataOutput: !this.dataOut.isNull() - ? base64ArrayBuffer(Memory.readByteArray(this.dataOut, this.dataOutLength.readU32())) + ? base64ArrayBuffer(this.dataOut.readByteArray(this.dataOutLength.readU32())) : null }; send(JSON.stringify({'[MBSFDUMP] crypto': cccrypt_re})); @@ -60,8 +60,8 @@ try { CCOperation: args[0].toInt32(), CCAlgorithm: args[1].toInt32(), CCOptions: args[2].toInt32(), - Key: !args[3].isNull() ? base64ArrayBuffer(Memory.readByteArray(args[3], args[4].toInt32())) : null, - IV: !args[5].isNull() ? base64ArrayBuffer(Memory.readByteArray(args[5], 16)) : null + Key: !args[3].isNull() ? base64ArrayBuffer(args[3].readByteArray(args[4].toInt32())) : null, + IV: !args[5].isNull() ? base64ArrayBuffer(args[5].readByteArray(16)) : null }; send(JSON.stringify({'[MBSFDUMP] crypto': cccryptorcreate})); } @@ -75,7 +75,7 @@ try { this.len = args[5]; const update = { dataInput: !args[1].isNull() - ? base64ArrayBuffer(Memory.readByteArray(args[1], args[2].toInt32())) + ? base64ArrayBuffer(args[1].readByteArray(args[2].toInt32())) : null }; send(JSON.stringify({'[MBSFDUMP] crypto': update})); @@ -83,7 +83,7 @@ try { onLeave: function(retval) { const updateOut = { dataOutput: !this.out.isNull() - ? base64ArrayBuffer(Memory.readByteArray(this.out, this.len.readU32())) + ? base64ArrayBuffer(this.out.readByteArray(this.len.readU32())) : null }; send(JSON.stringify({'[MBSFDUMP] crypto': updateOut})); @@ -100,7 +100,7 @@ try { onLeave: function(retval) { const finalOut = { dataOutput: !this.out2.isNull() - ? base64ArrayBuffer(Memory.readByteArray(this.out2, this.len2.readU32())) + ? base64ArrayBuffer(this.out2.readByteArray(this.len2.readU32())) : null }; send(JSON.stringify({'[MBSFDUMP] crypto': finalOut})); @@ -126,7 +126,7 @@ try { operation: 'CC_SHA1_Update', contextAddress: args[0], data: !args[1].isNull() - ? base64ArrayBuffer(Memory.readByteArray(args[1], args[2].toInt32())) + ? base64ArrayBuffer(args[1].readByteArray(args[2].toInt32())) : null }; send(JSON.stringify({'[MBSFDUMP] crypto': update})); @@ -145,7 +145,7 @@ try { operation: 'CC_SHA1_Final', contextAddress: this.ctxSha, hash: !this.mdSha.isNull() - ? base64ArrayBuffer(Memory.readByteArray(this.mdSha, 20)) + ? base64ArrayBuffer(this.mdSha.readByteArray(20)) : null }; send(JSON.stringify({'[MBSFDUMP] crypto': shaFinal})); @@ -161,7 +161,7 @@ try { CC_SHA1_Update(); CC_SHA1_Final(); - send("iOS crypto dumper loaded successfully"); + send("Dumping IOS Crypto Operations"); } catch (e) { send("Error loading iOS crypto dumper: " + e); } \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/data-dir.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/data-dir.js index dfbbe659..c72b171b 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/data-dir.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/data-dir.js @@ -5,10 +5,10 @@ * */ function listDirectoryContentsAtPath(path) { - var fileManager = ObjC.classes.NSFileManager.defaultManager(); - var enumerator = fileManager.enumeratorAtPath_(path); - var file; - var paths = []; + const fileManager = ObjC.classes.NSFileManager.defaultManager(); + const enumerator = fileManager.enumeratorAtPath_(path); + let file; + const paths = []; while ((file = enumerator.nextObject()) !== null) { paths.push(path + '/' + file); @@ -18,32 +18,33 @@ function listDirectoryContentsAtPath(path) { } function listHomeDirectoryContents() { - var homePath = ObjC.classes.NSProcessInfo.processInfo().environment().objectForKey_("HOME").toString(); - var paths = listDirectoryContentsAtPath(homePath); + const homePath = ObjC.classes.NSProcessInfo.processInfo().environment().objectForKey_("HOME").toString(); + const paths = listDirectoryContentsAtPath(homePath); return paths; } function getDataProtectionKeyForPath(path) { - var fileManager = ObjC.classes.NSFileManager.defaultManager(); - var urlPath = ObjC.classes.NSURL.fileURLWithPath_(path); - var fileProtectionKey = ObjC.Object(ptr(fileManager.attributesOfItemAtPath_error_(urlPath.path(), NULL))); - var protString = fileProtectionKey.valueForKey_("NSFileProtectionKey") - if (protString) - return protString.UTF8String(); - else{ - return ''; + const fileManager = ObjC.classes.NSFileManager.defaultManager(); + const urlPath = ObjC.classes.NSURL.fileURLWithPath_(path); + const attributeDict = fileManager.attributesOfItemAtPath_error_(urlPath.path(), NULL); + if (attributeDict) { + const protString = attributeDict.objectForKey_("NSFileProtectionKey"); + if (protString) { + return protString.UTF8String(); + } } + return ''; } function getDataProtectionKeysForAllPaths() { - var fileManager = ObjC.classes.NSFileManager.defaultManager(); - var dict = []; - var paths = listHomeDirectoryContents(); + const fileManager = ObjC.classes.NSFileManager.defaultManager(); + const dict = []; + const paths = listHomeDirectoryContents(); - var isDir = Memory.alloc(Process.pointerSize); + const isDir = Memory.alloc(Process.pointerSize); isDir.writePointer(NULL); - for (var i = 0; i < paths.length; i++) { + for (let i = 0; i < paths.length; i++) { fileManager.fileExistsAtPath_isDirectory_(paths[i], isDir); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/json.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/json.js index 91d92646..e34d8098 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/json.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/json.js @@ -5,7 +5,7 @@ function traceJSON(){ { onEnter: function(args) { var jsonData = ObjC.Object(args[2]); - var jsonStr = Memory.readUtf8String(jsonData.bytes(), jsonData.length()); + var jsonStr = jsonData.bytes().readUtf8String(jsonData.length()); send(JSON.stringify({'[MBSFDUMP] json': jsonStr})); } }); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/keychain.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/keychain.js index 2db72747..e4443088 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/keychain.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/keychain.js @@ -136,11 +136,11 @@ function dumpKeyChain(){ // } // try and get a string representation of the data - try { - + try { + var data_instance = new ObjC.Object(raw_data); - return Memory.readUtf8String(data_instance.bytes(), data_instance.length()); - + return data_instance.bytes().readUtf8String(data_instance.length()); + } catch (_) { try { @@ -271,7 +271,7 @@ function dumpKeyChain(){ } // read the resultant dict of the lookup from memory - var search_results = new ObjC.Object(Memory.readPointer(results_pointer)); + var search_results = new ObjC.Object(results_pointer.readPointer()); // if there are search results, loop them each and populate the return // array with the data we got diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/nslog.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/nslog.js index c805b2a9..396ed90b 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/nslog.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/nslog.js @@ -17,7 +17,7 @@ try { Interceptor.attach(foundation.getExportByName("NSLog"), { onEnter: function(args) { - send(JSON.stringify({'[MBSFDUMP] nslog': 'NSLog -> ' + ObjC.Object(ptr(args[0])).toString() + ', ' + Memory.readCString(ptr(args[1]))})); + send(JSON.stringify({'[MBSFDUMP] nslog': 'NSLog -> ' + ObjC.Object(ptr(args[0])).toString() + ', ' + ptr(args[1]).readCString()})); }, onLeave: function(retval) { } @@ -25,7 +25,7 @@ try { Interceptor.attach(foundation.getExportByName("NSLogv"), { onEnter: function(args) { - send(JSON.stringify({'[MBSFDUMP] nslog': 'NSLogv -> ' + ObjC.Object(ptr(args[0])).toString()+ ', ' + Memory.readCString(ptr(args[1]))})); + send(JSON.stringify({'[MBSFDUMP] nslog': 'NSLogv -> ' + ObjC.Object(ptr(args[0])).toString()+ ', ' + ptr(args[1]).readCString()})); }, onLeave: function(retval) { } diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-flutter.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-flutter.js index 33275fbf..25f21e29 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-flutter.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-flutter.js @@ -26,18 +26,14 @@ function hook_ssl_verify_result(address) function disablePinning() { var pattern = "FF 03 05 D1 FC 6F 0F A9 F8 5F 10 A9 F6 57 11 A9 F4 4F 12 A9 FD 7B 13 A9 FD C3 04 91 08 0A 80 52" - Process.enumerateRangesSync('r-x').filter(function (m) - { - if (m.file) return m.file.path.indexOf('Flutter') > -1; - return false; - }).forEach(function (r) - { - Memory.scanSync(r.base, r.size, pattern).forEach(function (match) { - send('[+] ssl_verify_result found at: ' + match.address.toString()); - hook_ssl_verify_result(match.address); - send('[*] Started: Bypass Flutter SSL-Pinning'); - - }); - }); + for (const r of Process.enumerateRanges('r-x')) { + if (r.file && r.file.path.includes('Flutter')) { + for (const match of Memory.scan(r.base, r.size, pattern)) { + send('[+] ssl_verify_result found at: ' + match.address.toString()); + hook_ssl_verify_result(match.address); + send('[*] Started: Bypass Flutter SSL-Pinning'); + } + } + } } setTimeout(disablePinning, 1000) diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios10.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios10.js deleted file mode 100644 index 2ef0cc82..00000000 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios10.js +++ /dev/null @@ -1,23 +0,0 @@ -/************************************************************************ - * Name: SSL Pinning Bypass for iOS 10 - * OS: iOS - * Author: @dki - * Source: https://codeshare.frida.re/@dki/ios10-ssl-bypass/ - * Modified to support Frida 17.0.0+ -*************************************************************************/ - -try { - var tls_helper_create_peer_trust = new NativeFunction( - Module.getGlobalExportByName("tls_helper_create_peer_trust"), - 'int', ['pointer', 'bool', 'pointer'] - ); - - var errSecSuccess = 0; - - Interceptor.replace(tls_helper_create_peer_trust, new NativeCallback(function(hdsk, server, trustRef) { - return errSecSuccess; - }, 'int', ['pointer', 'bool', 'pointer'])); - send("SSL certificate validation bypass active"); -} catch (e) { - send("Error loading iOS 10 SSL bypass: " + e); -} \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios11.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios11.js deleted file mode 100644 index 58dbd52d..00000000 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios11.js +++ /dev/null @@ -1,24 +0,0 @@ -/************************************************************************ - * Name: SSL Pinning Bypass for iOS 11 - * OS: iOS - * Author: @dki - * Source: https://codeshare.frida.re/@dki/ios10-ssl-bypass/ - * Modified to support Frida 17.0.0+ -*************************************************************************/ - -try { - /* OSStatus nw_tls_create_peer_trust(tls_handshake_t hdsk, bool server, SecTrustRef *trustRef); */ - var tls_helper_create_peer_trust = new NativeFunction( - Module.getGlobalExportByName("nw_tls_create_peer_trust"), - 'int', ['pointer', 'bool', 'pointer'] - ); - - var errSecSuccess = 0; - - Interceptor.replace(tls_helper_create_peer_trust, new NativeCallback(function(hdsk, server, trustRef) { - return errSecSuccess; - }, 'int', ['pointer', 'bool', 'pointer'])); - send("SSL certificate validation bypass active"); -} catch (e) { - send("Error loading iOS 11 SSL bypass: " + e); -} \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios12.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios12.js deleted file mode 100644 index 04d5b0d4..00000000 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios12.js +++ /dev/null @@ -1,58 +0,0 @@ -/************************************************************************ - * Name: SSL Pinning Bypass for iOS 12 - * OS: iOS - * Author: Github @machoreverser / twitter @macho_reverser - * Source: https://github.com/machoreverser/Frida-Scripts - * Info: - iOS 12 SSL Bypass based on blog post - https://nabla-c0d3.github.io/blog/2019/05/18/ssl-kill-switch-for-ios12/ - * Modified to support Frida 17.0.0+ -*************************************************************************/ - -// Variables -var SSL_VERIFY_NONE = 0; -var ssl_ctx_set_custom_verify; -var ssl_get_psk_identity; - -try { - const libboringssl = Process.getModuleByName("libboringssl.dylib"); - - /* Create SSL_CTX_set_custom_verify NativeFunction - * Function signature https://github.com/google/boringssl/blob/7540cc2ec0a5c29306ed852483f833c61eddf133/include/openssl/ssl.h#L2294 - */ - ssl_ctx_set_custom_verify = new NativeFunction( - libboringssl.getExportByName("SSL_CTX_set_custom_verify"), - 'void', ['pointer', 'int', 'pointer'] - ); - - /* Create SSL_get_psk_identity NativeFunction - * Function signature https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_get_psk_identity - */ - ssl_get_psk_identity = new NativeFunction( - libboringssl.getExportByName("SSL_get_psk_identity"), - 'pointer', ['pointer'] - ); - - /** Custom callback passed to SSL_CTX_set_custom_verify */ - function custom_verify_callback_that_does_not_validate(ssl, out_alert){ - return SSL_VERIFY_NONE; - } - - /** Wrap callback in NativeCallback for frida */ - var ssl_verify_result_t = new NativeCallback(function (ssl, out_alert){ - custom_verify_callback_that_does_not_validate(ssl, out_alert); - },'int',['pointer','pointer']); - - Interceptor.replace(ssl_ctx_set_custom_verify, new NativeCallback(function(ssl, mode, callback) { - // |callback| performs the certificate verification. Replace this with our custom callback - ssl_ctx_set_custom_verify(ssl, mode, ssl_verify_result_t); - }, 'void', ['pointer', 'int', 'pointer'])); - - Interceptor.replace(ssl_get_psk_identity, new NativeCallback(function(ssl) { - return "notarealPSKidentity"; - }, 'pointer', ['pointer'])); - - send("[+] iOS 12 SSL Pinning Bypass - successfully loaded"); -} catch (e) { - send("Error loading iOS 12 SSL bypass: " + e); -} \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios13.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios13.js deleted file mode 100755 index d59cea76..00000000 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-ios13.js +++ /dev/null @@ -1,51 +0,0 @@ -/* Description: iOS 13 SSL Bypass based on https://codeshare.frida.re/@machoreverser/ios12-ssl-bypass/ and https://github.com/nabla-c0d3/ssl-kill-switch2 - * Author: @apps3c - * Modified to support Frida 17.0.0+ - */ - -try { - // Module.ensureInitialized is deprecated and no longer needed - var libboringssl = Process.getModuleByName("libboringssl.dylib"); -} catch(err) { - send("libboringssl.dylib module not loaded. Trying to manually load it.") - Module.load("libboringssl.dylib"); - var libboringssl = Process.getModuleByName("libboringssl.dylib"); -} - -var SSL_VERIFY_NONE = 0; -var ssl_set_custom_verify; -var ssl_get_psk_identity; - -ssl_set_custom_verify = new NativeFunction( - libboringssl.getExportByName("SSL_set_custom_verify"), - 'void', ['pointer', 'int', 'pointer'] -); - -/* Create SSL_get_psk_identity NativeFunction -* Function signature https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#SSL_get_psk_identity -*/ -ssl_get_psk_identity = new NativeFunction( - libboringssl.getExportByName("SSL_get_psk_identity"), - 'pointer', ['pointer'] -); - -/** Custom callback passed to SSL_CTX_set_custom_verify */ -function custom_verify_callback_that_does_not_validate(ssl, out_alert){ - return SSL_VERIFY_NONE; -} - -/** Wrap callback in NativeCallback for frida */ -var ssl_verify_result_t = new NativeCallback(function (ssl, out_alert){ - custom_verify_callback_that_does_not_validate(ssl, out_alert); -},'int',['pointer','pointer']); - -Interceptor.replace(ssl_set_custom_verify, new NativeCallback(function(ssl, mode, callback) { - // |callback| performs the certificate verification. Replace this with our custom callback - ssl_set_custom_verify(ssl, mode, ssl_verify_result_t); -}, 'void', ['pointer', 'int', 'pointer'])); - -Interceptor.replace(ssl_get_psk_identity, new NativeCallback(function(ssl) { - return "notarealPSKidentity"; -}, 'pointer', ['pointer'])); - -send("[+] Bypass successfully loaded "); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-trustkit.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-trustkit.js index 30b65113..684166fe 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-trustkit.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/bypass-ssl-trustkit.js @@ -1,33 +1,45 @@ -/* +/* Description: iOS TrustKit Certificate Pinning ByPass - Usage: frida -U -f XXX -l ios-trustkit-pinning-bypass.js - Credit: Unknown - Src: https://github.com/rsenet/FriList/blob/main/02_SecurityBypass/CertificatePinning/ios-trustkit-pinning-bypass.js + Updated for Frida 17+ */ -if (ObjC.available) -{ - console.log("SSLUnPinning Enabled"); +function bypassTrustKit() { + if (!ObjC.available) { + send("❌ Objective-C Runtime is not available!"); + return; + } - for (var className in ObjC.classes) - { - if (ObjC.classes.hasOwnProperty(className)) - { - if (className == "TrustKit") - { - console.log("Found our target class : " + className); - var hook = ObjC.classes.TrustKit["+ initSharedInstanceWithConfiguration:"]; + send("🔐 SSLUnPinning Enabled"); - Interceptor.replace(hook.implementation, new NativeCallback(function() - { - console.log("Hooking TrustKit"); + try { + const classMap = ObjC.enumerateLoadedClassesSync(); + + for (const image in classMap) { + for (const className of classMap[image]) { + if (className === "TrustKit") { + send("✅ Found TrustKit class in: " + image); + + const method = ObjC.classes.TrustKit["+ initSharedInstanceWithConfiguration:"]; + if (!method || method.implementation.isNull()) { + send("❌ TrustKit method not found or invalid."); + return; + } + + Interceptor.replace(method.implementation, new NativeCallback(function () { + send("✅ Hooked TrustKit: +initSharedInstanceWithConfiguration:"); + return; + }, 'int', [])); + + send("✅ TrustKit bypass hook installed."); return; - }, 'int', [])); + } } } + + send("❌ TrustKit class not found."); + } catch (err) { + send("❌ Error during TrustKit bypass: " + err.message); } -} -else -{ - console.log("Objective-C Runtime is not available!"); -} \ No newline at end of file +} + +setImmediate(bypassTrustKit); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-http-clients.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-http-clients.js new file mode 100644 index 00000000..0875fa91 --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-http-clients.js @@ -0,0 +1,57 @@ +function detect_network_libraries() { + if (!ObjC.available) { + send("❌ Objective-C runtime is not available."); + return; + } + + const knownLibs = { + "NSURLSession": "Apple (Modern HTTP API)", + "NSURLConnection": "Apple (Deprecated HTTP API)", + "CFNetwork": "Apple (C-level networking)", + "AFURLSessionManager": "AFNetworking (ObjC)", + "AFHTTPSessionManager": "AFNetworking (ObjC)", + "Alamofire.Session": "Alamofire (Swift)", + "Alamofire.Request": "Alamofire (Swift)", + "SDWebImageDownloader": "SDWebImage (HTTP Image Fetcher)", + "SocketRocket.SRWebSocket": "SocketRocket (WebSockets)", + "GRPCClient": "gRPC (Objective-C)", + "GTMSessionFetcher": "Google API Client", + "FIRMessagingConnection": "Firebase Messaging", + "FBSDKGraphRequest": "Facebook SDK", + "AWSNetworking": "AWS iOS SDK", + "ASIHTTPRequest": "ASIHTTPRequest (Legacy ObjC)" + }; + + send("🔍 Scanning for known networking libraries..."); + + const present = []; + + for (const className in knownLibs) { + if (ObjC.classes.hasOwnProperty(className)) { + present.push({ name: className, source: knownLibs[className] }); + } + } + + if (present.length === 0) { + send("❌ No known networking classes found."); + } else { + send("✅ Detected networking classes/libraries:"); + present.forEach(entry => { + send(` • ${entry.name} ➜ ${entry.source}`); + }); + } + + // Extra: enumerate loaded modules that may hint at networking libs + const loadedModules = Process.enumerateModules(); + const moduleHints = ["AFNetworking", "Alamofire", "CFNetwork", "GTMSession", "SocketRocket", "libcurl", "libgrpc"]; + + loadedModules.forEach(mod => { + moduleHints.forEach(hint => { + if (mod.name.indexOf(hint) !== -1) { + send(`📦 Loaded module: ${mod.name} (possible: ${hint})`); + } + }); + }); +} + +setImmediate(detect_network_libraries); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-network.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-network.js new file mode 100644 index 00000000..c9f09353 --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-network.js @@ -0,0 +1,27 @@ +function safeToString(obj) { + return obj?.toString?.() ?? "null"; +} + + +const host = ObjC.classes.NSHost.currentHost(); +send("Host name: " + safeToString(host.name())); + +const addresses = host.addresses(); +for (let i = 0; i < addresses.count(); i++) { + send("Address: " + safeToString(addresses.objectAtIndex_(i))); +} + +const networkInfo = ObjC.classes.CTTelephonyNetworkInfo.alloc().init(); +const providers = networkInfo.serviceSubscriberCellularProviders(); +const keys = providers.allKeys(); + +for (let j = 0; j < keys.count(); j++) { + const key = keys.objectAtIndex_(j); + const carrier = providers.objectForKey_(key); + send("Carrier for " + safeToString(key) + ":"); + send(" Carrier Name: " + safeToString(carrier.carrierName())); + send(" Mobile Country Code: " + safeToString(carrier.mobileCountryCode())); + send(" Mobile Network Code: " + safeToString(carrier.mobileNetworkCode())); + send(" ISO Country Code: " + safeToString(carrier.isoCountryCode())); + send(" Allows VOIP: " + carrier.allowsVOIP()); +} diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes-methods.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes-methods.js index 332c15f7..a5aa3be4 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes-methods.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes-methods.js @@ -1,38 +1,55 @@ -/* Description: Dump all methods inside all classes - * Mode: S+A - * Version: 1.0 - * Credit: https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -function run_show_classes_methods_of_app() -{ - send("Enumerating Classes and Methods") - for (var className in ObjC.classes) - { - if (ObjC.classes.hasOwnProperty(className)) - { - send("[AUXILIARY] Class: " + className); - //var methods = ObjC.classes[className].$methods; - var methods = ObjC.classes[className].$ownMethods; - for (var i = 0; i < methods.length; i++) - { - send("[AUXILIARY] \t Method: " + methods[i]); - try - { - send("[AUXILIARY] \t\tArguments Type: " + ObjC.classes[className][methods[i]].argumentTypes); - send("[AUXILIARY] \t\tReturn Type: " + ObjC.classes[className][methods[i]].returnType); - } - catch(err) {} - } - } - } - send("Completed Enumerating Methods of All Classes") +function run_show_classes_methods_of_app() { + send("Enumerating Classes and Methods"); + + let count = 0; + + try { + const classesByImage = ObjC.enumerateLoadedClassesSync(); + + for (const imageName in classesByImage) { + // Only inspect classes from the app itself + if (!imageName.includes(".app")) continue; + + const classList = classesByImage[imageName]; + + for (const className of classList) { + try { + const klass = ObjC.classes[className]; + if (!klass) continue; + + send("[AUXILIARY] Class: " + className); + count++; + + const methods = klass.$ownMethods; + + for (let i = 0; i < methods.length; i++) { + const methodName = methods[i]; + send("[AUXILIARY] \t Method: " + methodName); + + try { + const method = klass[methodName]; + send("[AUXILIARY] \t\tArguments Type: " + method.argumentTypes); + send("[AUXILIARY] \t\tReturn Type: " + method.returnType); + } catch (err) { + send("[AUXILIARY] \t\t Error retrieving types: " + err.message); + } + } + } catch (err) { + send("[AUXILIARY] Error accessing class: " + className + " — " + err.message); + } + } + } + + send("[AUXILIARY] \n Classes found: " + count); + } catch (err) { + send("Failed to enumerate classes: " + err.message); + } + + send("Completed Enumerating Methods of All Classes"); } -function show_classes_methods_of_app() -{ - setImmediate(run_show_classes_methods_of_app) +function show_classes_methods_of_app() { + setImmediate(run_show_classes_methods_of_app); } -show_classes_methods_of_app() \ No newline at end of file + +show_classes_methods_of_app(); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes.js index 7c4dfb9a..f579c525 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/find-all-classes.js @@ -1,30 +1,32 @@ -/* Description: Dump all classes used by the app - * Mode: S+A - * Version: 1.0 - * Credit: https://github.com/interference-security/frida-scripts/blob/master/iOS - * Author: @interference-security - */ -//Twitter: https://twitter.com/xploresec -//GitHub: https://github.com/interference-security -function run_show_classes_of_app() -{ - send("Enumerating Classes") - var count = 0 - for (var className in ObjC.classes) - { - if (ObjC.classes.hasOwnProperty(className)) - { - send("[AUXILIARY] " + className); - count = count + 1 +function run_show_classes_of_app() { + send("Enumerating Classes"); + + try { + const classesByImage = ObjC.enumerateLoadedClassesSync(); + let count = 0; + + // Filter to only app-specific modules (e.g., containing ".app") + for (const imageName in classesByImage) { + if (!imageName.includes(".app")) continue; + + const classList = classesByImage[imageName]; + + for (const className of classList) { + send("[AUXILIARY] " + className); + count++; + } } + + send("[AUXILIARY] \n Classes found: " + count); + } catch (err) { + send("❌ Error during class enumeration: " + err.message); } - send("[AUXILIARY] \n Classes found: " + count); - send("Completed Enumerating Classes") + + send("Completed Enumerating Classes"); } -function show_classes_of_app() -{ - setImmediate(run_show_classes_of_app) +function show_classes_of_app() { + setImmediate(run_show_classes_of_app); } -show_classes_of_app() \ No newline at end of file +show_classes_of_app(); \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-exports.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-exports.js index a6d91d4c..98185077 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-exports.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-exports.js @@ -1,7 +1,8 @@ // Get Modules and Exports // Based on: https://github.com/iddoeldor/frida-snippets +// Updated for Frida 17.0.0+ var x = {}; -Process.enumerateModulesSync().forEach(function(m){ - x[m.name] = Module.enumerateExportsSync(m.name) -}); +for (const m of Process.enumerateModules()) { + x[m.name] = m.enumerateExports(); +} console.log(JSON.stringify(x, null, ' ')) \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-modules.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-modules.js index 3e3b982a..6486f4e6 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-modules.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/get-modules.js @@ -1,5 +1,6 @@ // Based on https://github.com/iddoeldor/frida-snippets#list-modules -Process.enumerateModulesSync() +// Updated for Frida 17.0.0+ compatibility +Process.enumerateModules() .filter(function(m){ return m['path'].toLowerCase().indexOf('app') !=-1 ; }) .forEach(function(m) { send(JSON.stringify(m, null, ' ')); diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/trace-bluetooth.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/trace-bluetooth.js index af91841d..5d7de495 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/trace-bluetooth.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/trace-bluetooth.js @@ -31,7 +31,7 @@ Interceptor.attach(ObjC.classes.CBPeripheral['- writeValue:forCharacteristic:typ { var data = new ObjC.Object(args[2]); var CBChar = new ObjC.Object(args[3]); - var dataBytes = Memory.readByteArray(data.bytes(), data.length()); + var dataBytes = data.bytes().readByteArray(data.length()); var b = new Uint8Array(dataBytes); var hexData = ""; diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/screenshot.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/screenshot.js new file mode 100644 index 00000000..cba9e229 --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/screenshot.js @@ -0,0 +1,92 @@ +getScreenshot: function (){ + let cachedApi = null; + const CGFloat = (Process.pointerSize === 4) ? 'float' : 'double'; + const CGSize = [CGFloat, CGFloat]; + function getUIKitApi() { + if (cachedApi !== null) return cachedApi; + + const uikit = Process.getModuleByName('UIKit'); + cachedApi = { + UIApplication: ObjC.classes.UIApplication, + UIWindow: ObjC.classes.UIWindow, + NSThread: ObjC.classes.NSThread, + UIGraphicsBeginImageContextWithOptions: new NativeFunction( + uikit.getExportByName('UIGraphicsBeginImageContextWithOptions'), + 'void', [CGSize, 'bool', CGFloat] + ), + UIGraphicsEndImageContext: new NativeFunction( + uikit.getExportByName('UIGraphicsEndImageContext'), + 'void', [] + ), + UIGraphicsGetImageFromCurrentImageContext: new NativeFunction( + uikit.getExportByName('UIGraphicsGetImageFromCurrentImageContext'), + 'pointer', [] + ), + UIImagePNGRepresentation: new NativeFunction( + uikit.getExportByName('UIImagePNGRepresentation'), + 'pointer', ['pointer'] + ) + }; + + return cachedApi; + } + + function performOnMainThread(action) { + const api = getUIKitApi(); + if (api.NSThread.isMainThread()) { + action(); + } else { + ObjC.schedule(ObjC.mainQueue, action); + } + } + + function captureScreenshot(view = null) { + const api = getUIKitApi(); + + if (!view) { + const windows = api.UIApplication.sharedApplication().windows(); + for (let i = 0; i < windows.count(); i++) { + const win = windows.objectAtIndex_(i); + if (win.isKeyWindow()) { + view = win; + break; + } + } + } + + if (!view) { + console.log("❌ No key window found."); + return; + } + + const bounds = view.bounds(); + const size = bounds[1]; + api.UIGraphicsBeginImageContextWithOptions(size, 0, 0); + view.drawViewHierarchyInRect_afterScreenUpdates_(bounds, true); + const image = api.UIGraphicsGetImageFromCurrentImageContext(); + api.UIGraphicsEndImageContext(); + + const pngData = new ObjC.Object(api.UIImagePNGRepresentation(image)); + const bytePtr = pngData.bytes(); + const buffer = bytePtr.readByteArray(pngData.length()); + const path = "/tmp/screenshot.png"; + const f = new File(path, "wb"); + f.write(buffer); + f.flush(); + f.close(); + + send("✅ Screenshot Captured"); + } + + + function capture(){ + performOnMainThread(() => { + try { + captureScreenshot(); + } catch (e) { + console.log("❌ Error capturing screenshot: " + e.message); + } + }); + } + capture(); +} \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/ssl-pinning.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/ssl-pinning.js new file mode 100644 index 00000000..4d72b3ec --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/rpc/ssl-pinning.js @@ -0,0 +1,265 @@ +bypassIosSslPinning: function() { + + // Constants + const SSL_VERIFY_NONE = 0; + const ERR_SEC_SUCCESS = 0; + + function get_ios_version() { + try { + // Get iOS version using UIDevice + const UIDevice = ObjC.classes.UIDevice; + const device = UIDevice.currentDevice(); + const systemVersion = device.systemVersion().toString(); + send("[+] Detected iOS version: " + systemVersion); + + // Parse version string to get major version + const majorVersion = parseInt(systemVersion.split('.')[0]); + return majorVersion; + } catch (e) { + send("[-] Error detecting iOS version: " + e); + // Fallback: try to detect from system info + try { + const sysinfo = Process.getSystemInfo(); + if (sysinfo && sysinfo.os) { + const versionMatch = sysinfo.os.match(/iOS (\d+)/); + if (versionMatch) { + const majorVersion = parseInt(versionMatch[1]); + send("[+] Detected iOS version from system info: " + majorVersion); + return majorVersion; + } + } + } catch (fallbackError) { + send("[-] Fallback iOS version detection failed: " + fallbackError); + } + + // Default to latest version if detection fails + send("[!] Could not detect iOS version, defaulting to iOS 15+ bypass"); + return 15; + } + } + + // Helper function to create custom verify callback + function createCustomVerifyCallback() { + return new NativeCallback(function(ssl, out_alert) { + return SSL_VERIFY_NONE; + }, 'int', ['pointer', 'pointer']); + } + + // Helper function to create PSK identity callback + function createPSKIdentityCallback() { + return new NativeCallback(function(ssl) { + return "notarealPSKidentity"; + }, 'pointer', ['pointer']); + } + + function bypass_ssl_pinning_ios10() { + try { + const tls_helper_create_peer_trust = new NativeFunction( + Module.getGlobalExportByName("tls_helper_create_peer_trust"), + 'int', ['pointer', 'bool', 'pointer'] + ); + + Interceptor.replace(tls_helper_create_peer_trust, new NativeCallback(function(hdsk, server, trustRef) { + return ERR_SEC_SUCCESS; + }, 'int', ['pointer', 'bool', 'pointer'])); + send("[+] iOS 10 SSL certificate validation bypass active"); + } catch (e) { + send("[-] Error loading iOS 10 SSL bypass: " + e); + } + } + + function bypass_ssl_pinning_ios11() { + try { + const tls_helper_create_peer_trust = new NativeFunction( + Module.getGlobalExportByName("nw_tls_create_peer_trust"), + 'int', ['pointer', 'bool', 'pointer'] + ); + + Interceptor.replace(tls_helper_create_peer_trust, new NativeCallback(function(hdsk, server, trustRef) { + return ERR_SEC_SUCCESS; + }, 'int', ['pointer', 'bool', 'pointer'])); + send("[+] iOS 11 SSL certificate validation bypass active"); + } catch (e) { + send("[-] Error loading iOS 11 SSL bypass: " + e); + } + } + + function bypass_ssl_pinning_ios12() { + try { + const libboringssl = Process.getModuleByName("libboringssl.dylib"); + if (!libboringssl) { + send("[-] libboringssl.dylib not found for iOS 12 bypass"); + return; + } + + const ssl_ctx_set_custom_verify = new NativeFunction( + libboringssl.getExportByName("SSL_CTX_set_custom_verify"), + 'void', ['pointer', 'int', 'pointer'] + ); + + const ssl_get_psk_identity = new NativeFunction( + libboringssl.getExportByName("SSL_get_psk_identity"), + 'pointer', ['pointer'] + ); + + const ssl_verify_result_t = createCustomVerifyCallback(); + const psk_callback = createPSKIdentityCallback(); + + Interceptor.replace(ssl_ctx_set_custom_verify, new NativeCallback(function(ssl, mode, callback) { + ssl_ctx_set_custom_verify(ssl, mode, ssl_verify_result_t); + }, 'void', ['pointer', 'int', 'pointer'])); + + Interceptor.replace(ssl_get_psk_identity, psk_callback); + + send("[+] iOS 12 SSL Pinning Bypass - successfully loaded"); + } catch (e) { + send("[-] Error loading iOS 12 SSL bypass: " + e); + } + } + + function bypass_ssl_pinning_ios13() { + try { + let libboringssl = Process.getModuleByName("libboringssl.dylib"); + if (!libboringssl) { + send("[!] libboringssl.dylib module not loaded. Trying to manually load it."); + Module.load("libboringssl.dylib"); + libboringssl = Process.getModuleByName("libboringssl.dylib"); + if (!libboringssl) { + send("[-] Failed to load libboringssl.dylib"); + return; + } + } + + const ssl_set_custom_verify = new NativeFunction( + libboringssl.getExportByName("SSL_set_custom_verify"), + 'void', ['pointer', 'int', 'pointer'] + ); + + const ssl_get_psk_identity = new NativeFunction( + libboringssl.getExportByName("SSL_get_psk_identity"), + 'pointer', ['pointer'] + ); + + const ssl_verify_result_t = createCustomVerifyCallback(); + const psk_callback = createPSKIdentityCallback(); + + Interceptor.replace(ssl_set_custom_verify, new NativeCallback(function(ssl, mode, callback) { + ssl_set_custom_verify(ssl, mode, ssl_verify_result_t); + }, 'void', ['pointer', 'int', 'pointer'])); + + Interceptor.replace(ssl_get_psk_identity, psk_callback); + + send("[+] iOS 13 SSL bypass successfully loaded"); + } catch (e) { + send("[-] Error loading iOS 13 SSL bypass: " + e); + } + } + + function bypass_ssl_pinning_ios14_plus() { + try { + const boringssl = Process.getModuleByName('libboringssl.dylib'); + if (!boringssl) { + send("[-] BoringSSL not found for iOS 14+ bypass"); + return; + } + + const SSL_set_custom_verify = new NativeFunction( + boringssl.getExportByName('SSL_set_custom_verify'), + 'void', ['pointer', 'int', 'pointer'] + ); + + const custom_verify_cb = createCustomVerifyCallback(); + + Interceptor.replace(SSL_set_custom_verify, new NativeCallback(function(ssl, mode, callback) { + SSL_set_custom_verify(ssl, mode, custom_verify_cb); + }, 'void', ['pointer', 'int', 'pointer'])); + + send('[+] BoringSSL SSL_set_custom_verify bypass installed.'); + } catch (e) { + send('[-] BoringSSL bypass failed: ' + e); + } + } + + // ===== Security.framework hooks ===== + function hookSecurityFramework() { + const securityHooks = [ + { + name: "SecTrustEvaluate", + callback: function(trust, resultPtr) { + if (resultPtr) { + Memory.writeU8(resultPtr, 1); // kSecTrustResultProceed + } + send("[+] SecTrustEvaluate() bypassed."); + return 0; // errSecSuccess + }, + returnType: 'int', + paramTypes: ['pointer', 'pointer'] + }, + { + name: "SecTrustEvaluateWithError", + callback: function(trust, result) { + if (result) { + Memory.writeU8(result, 1); // true + } + send("[+] SecTrustEvaluateWithError() bypassed."); + return 1; + }, + returnType: 'bool', + paramTypes: ['pointer', 'pointer'] + } + ]; + + // Apply security framework hooks + securityHooks.forEach(hook => { + try { + const func = Module.findExportByName("Security", hook.name); + if (func) { + Interceptor.replace(func, new NativeCallback(hook.callback, hook.returnType, hook.paramTypes)); + } + } catch (e) { + send(`[-] ${hook.name} not found or failed to hook: ${e.message}`); + } + }); + + // Hook SecTrustSetAnchorCertificates for monitoring + try { + const setAnchor = Module.findExportByName("Security", "SecTrustSetAnchorCertificates"); + if (setAnchor) { + Interceptor.attach(setAnchor, { + onEnter: function(args) { + send("[+] Attempt to set custom anchor certificates - bypassed."); + } + }); + } + } catch (e) { + send('[-] SecTrustSetAnchorCertificates not found or failed to hook: ' + e.message); + } + } + + // Main execution with optimizations + const iosVersion = get_ios_version(); + send("[+] Calling SSL pinning bypass for iOS " + iosVersion); + + // Always hook Security framework first (works across all iOS versions) + hookSecurityFramework(); + + // Call version-specific bypass with better error handling + try { + if (iosVersion <= 10) { + bypass_ssl_pinning_ios10(); + } else if (iosVersion === 11) { + bypass_ssl_pinning_ios11(); + } else if (iosVersion === 12) { + bypass_ssl_pinning_ios12(); + } else if (iosVersion === 13) { + bypass_ssl_pinning_ios13(); + } else if (iosVersion >= 14) { + bypass_ssl_pinning_ios14_plus(); + } + } catch (e) { + send("[-] Error during version-specific bypass: " + e); + } + + send("[+] SSL pinning bypass completed for iOS " + iosVersion); + +} \ No newline at end of file diff --git a/mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb b/mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb new file mode 100644 index 0000000000000000000000000000000000000000..e57eac5a55617b6d360e98b789d62d2eb5462b12 GIT binary patch literal 51658 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

x=2pY9|myAm1`8nN5b@>)lcJ5BAzpSg3PVzi6~ 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 literal 0 HcmV?d00001 diff --git a/mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.4.3_iphoneos-arm64.deb b/mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.4.3_iphoneos-arm64.deb new file mode 100644 index 0000000000000000000000000000000000000000..1c2af58a442619de695e1e0b356337ba477fe4b0 GIT binary patch literal 8470 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 CZw4O# literal 0 HcmV?d00001 diff --git a/mobsf/DynamicAnalyzer/views/android/environment.py b/mobsf/DynamicAnalyzer/views/android/environment.py index 340990d4..c62299c2 100644 --- a/mobsf/DynamicAnalyzer/views/android/environment.py +++ b/mobsf/DynamicAnalyzer/views/android/environment.py @@ -24,8 +24,8 @@ from mobsf.DynamicAnalyzer.tools.webproxy import ( start_proxy, stop_httptools, ) -from mobsf.DynamicAnalyzer.views.android import ( - frida_server_download as fserver, +from mobsf.DynamicAnalyzer.views.common.frida.server_update import ( + FridaServerUpdater, ) from mobsf.MobSF.utils import ( get_adb, @@ -699,7 +699,8 @@ class Environment: ' instance is running') return frida_bin = f'frida-server-{frida_version}-android-{frida_arch}' - stat = fserver.update_frida_server(frida_arch, frida_version) + stat = FridaServerUpdater( + 'android', frida_version).update_frida_server(frida_arch) if not stat: msg = ('Cannot download frida-server binary. You will need' f' {frida_bin} in {settings.DWD_DIR} for ' @@ -707,7 +708,7 @@ class Environment: logger.error(msg) return frida_path = os.path.join(settings.DWD_DIR, frida_bin) - logger.info('Copying frida server for %s', frida_arch) + logger.info('Copying frida server v%s for %s', frida_version, frida_arch) self.adb_command(['push', frida_path, '/system/fd_server']) self.adb_command(['chmod', '755', '/system/fd_server'], True) @@ -715,7 +716,7 @@ class Environment: """Start Frida Server.""" check = self.adb_command(['ps'], True) if b'fd_server' in check: - logger.info('Frida Server is already running') + logger.info('Frida Server v%s is already running', frida_version) return def start_frida(): @@ -729,6 +730,6 @@ class Environment: trd = threading.Thread(target=start_frida) trd.daemon = True trd.start() - logger.info('Starting Frida Server') + logger.info('Starting Frida Server v%s', frida_version) logger.info('Waiting for 2 seconds...') time.sleep(2) diff --git a/mobsf/DynamicAnalyzer/views/android/frida_server_download.py b/mobsf/DynamicAnalyzer/views/android/frida_server_download.py deleted file mode 100644 index 8c18ebc2..00000000 --- a/mobsf/DynamicAnalyzer/views/android/frida_server_download.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf_8 -*- -"""Download Frida Server.""" -import logging -from pathlib import Path -from lzma import LZMAFile -from shutil import copyfileobj - -import requests - -from django.conf import settings - -from mobsf.MobSF.utils import ( - is_internet_available, - upstream_proxy, -) - - -logger = logging.getLogger(__name__) - - -def clean_up_old_binaries(dirc, version): - """Delete Old Binaries.""" - for f in Path(dirc).iterdir(): - if f.is_file() and f.name.startswith('frida-server'): - if version in f.name: - continue - try: - f.unlink() - except Exception: - pass - - -def download_frida_server(url, version, fname, proxies, verify): - """Download frida-server-binary.""" - try: - download_dir = Path(settings.DWD_DIR) - logger.info('Downloading binary %s', fname) - dwd_loc = download_dir / fname - with requests.get( - url, - timeout=5, - proxies=proxies, - verify=verify, - stream=True) as r: - with LZMAFile(r.raw) as f: - with open(dwd_loc, 'wb') as flip: - copyfileobj(f, flip) - clean_up_old_binaries(download_dir, version) - return True - except Exception: - logger.exception('[ERROR] Downloading Frida Server Binary') - return False - - -def update_frida_server(arch, version): - """Download Assets of a given version.""" - download_dir = Path(settings.DWD_DIR) - fserver = f'frida-server-{version}-android-{arch}' - frida_bin = download_dir / fserver - if frida_bin.is_file(): - return True - if not is_internet_available(): - return False - try: - proxies, verify = upstream_proxy('https') - except Exception: - logger.exception('[ERROR] Setting upstream proxy') - try: - response = requests.get(f'{settings.FRIDA_SERVER}{version}', - timeout=5, - proxies=proxies, - verify=verify) - for item in response.json()['assets']: - if item['name'] == f'{fserver}.xz': - url = item['browser_download_url'] - return download_frida_server(url, version, fserver, proxies, verify) - return False - except Exception: - logger.exception('[ERROR] Fetching Frida Server Release') - return False diff --git a/mobsf/DynamicAnalyzer/views/common/device.py b/mobsf/DynamicAnalyzer/views/common/device.py index aa0acd16..486b4e82 100644 --- a/mobsf/DynamicAnalyzer/views/common/device.py +++ b/mobsf/DynamicAnalyzer/views/common/device.py @@ -54,9 +54,24 @@ def view_file(request, api=False): if not is_safe_path(src, sfile.as_posix(), fil): err = 'Path Traversal Attack Detected' return print_n_send_error_response(request, err, api) - dat = sfile.read_text('ISO-8859-1') - if fil.endswith('.plist') and dat.startswith('bplist0'): - dat = dumps(dat, fmt=FMT_XML).decode('utf-8', 'ignore') + # Check if it's a binary plist first + if fil.endswith('.plist'): + # Read as bytes to check for binary plist + raw_data = sfile.read_bytes() + if raw_data.startswith(b'bplist0'): + # It's a binary plist, load it properly + from plistlib import loads + try: + plist_data = loads(raw_data) + dat = dumps(plist_data, fmt=FMT_XML).decode('utf-8', 'ignore') + except Exception as e: + logger.warning('Failed to parse binary plist: %s', e) + dat = raw_data.decode('utf-8', 'ignore') + else: + # It's a text plist, read as text + dat = raw_data.decode('utf-8', 'ignore') + else: + dat = sfile.read_text('ISO-8859-1') if fil.endswith(('.xml', '.plist')) and typ in ['xml', 'plist']: rtyp = 'xml' elif typ == 'db': diff --git a/mobsf/DynamicAnalyzer/views/common/frida/server_update.py b/mobsf/DynamicAnalyzer/views/common/frida/server_update.py new file mode 100644 index 00000000..6390863f --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/common/frida/server_update.py @@ -0,0 +1,128 @@ +# -*- coding: utf_8 -*- +"""Common Frida Server Update Management for Android and iOS.""" + +import logging +from pathlib import Path +from lzma import LZMAFile +from shutil import copyfileobj + +import requests + +from django.conf import settings + +from mobsf.MobSF.utils import ( + is_internet_available, + upstream_proxy, +) + +logger = logging.getLogger(__name__) + + +class FridaServerUpdater: + """Class for managing Frida server updates and downloads.""" + + def __init__(self, platform, version): + """Initialize the FridaServerUpdater. + + Args: + platform (str): The platform ('android' or 'ios') + version (str): The Frida version to manage + """ + self.download_dir = Path(settings.DWD_DIR) + self.platform = platform + self.version = version + + def clean_up_old_binaries(self): + """Delete old Frida server binaries.""" + if self.platform == 'android': + file_pattern = 'frida-server*' + else: + file_pattern = 'frida_*' + for f in self.download_dir.glob(file_pattern): + if f.is_file() and self.version not in f.name: + try: + f.unlink() + except Exception: + pass + + def download_frida_server(self, url, fname, proxies, verify): + """Download Frida server binary.""" + try: + logger.info('Downloading Frida server v%s binary: %s', self.version, fname) + dwd_loc = self.download_dir / fname + + with requests.get( + url, + timeout=15, + proxies=proxies, + verify=verify, + stream=True) as r: + r.raise_for_status() + + with open(dwd_loc, 'wb') as f: + if fname.endswith('.deb'): + copyfileobj(r.raw, f) + else: + copyfileobj(LZMAFile(r.raw), f) + + self.clean_up_old_binaries() + return True + + except Exception: + logger.exception( + '[ERROR] Downloading Frida Server v%s Binary', self.version) + # Clean up partial download + try: + dwd_loc.unlink() + except Exception: + pass + return False + + def update_frida_server(self, arch): + """Update/download Frida server for the given architecture.""" + if self.platform == 'android': + fserver = f'frida-server-{self.version}-{self.platform}-{arch}' + else: + fserver = f'frida_{self.version}_{self.platform}-{arch}.deb' + frida_bin = self.download_dir / fserver + if frida_bin.is_file(): + return True + if not is_internet_available(): + return False + try: + proxies, verify = upstream_proxy('https') + except Exception: + logger.exception('[ERROR] Setting upstream proxy') + proxies, verify = None, True + + try: + # Get Frida release asset urls + response = requests.get( + f'{settings.FRIDA_SERVER}{self.version}', + timeout=5, + proxies=proxies, + verify=verify, + ) + response.raise_for_status() + + # Find the correct binary + if self.platform == 'android': + asset = f'{fserver}.xz' + else: + asset = fserver + for item in response.json()['assets']: + if item['name'] == asset: + return self.download_frida_server( + item['browser_download_url'], fserver, proxies, verify, + ) + + logger.error( + 'Frida server v%s binary not found for platform: %s, architecture: %s', + self.version, + self.platform, arch, + ) + + except Exception: + logger.exception('[ERROR] Fetching Frida Server v%s Release', self.version) + + return False diff --git a/mobsf/DynamicAnalyzer/views/common/shared.py b/mobsf/DynamicAnalyzer/views/common/shared.py index 181cf1c5..ef95b526 100644 --- a/mobsf/DynamicAnalyzer/views/common/shared.py +++ b/mobsf/DynamicAnalyzer/views/common/shared.py @@ -57,15 +57,15 @@ def safe_paths(tar_meta): yield fh -def onerror(func, path, exc_info): - _, exc, _ = exc_info - if exc.errno == errno.EACCES: # Permission error +def onexc(func, path, exc_info): + _, exc_value, _ = exc_info + if exc_value.errno == errno.EACCES: # Permission error try: os.chmod(path, 0o755) func(path) except Exception: pass - elif exc.errno == errno.ENOTEMPTY: # Directory not empty + elif exc_value.errno == errno.ENOTEMPTY: # Directory not empty try: func(path) except Exception: @@ -83,7 +83,7 @@ def untar_files(tar_loc, untar_dir): return False if untar_dir.exists(): # fix for permission errors - shutil.rmtree(untar_dir, onerror=onerror) + shutil.rmtree(untar_dir, onexc=onexc) else: os.makedirs(untar_dir) with tarfile.open(tar_loc.as_posix(), errorlevel=1) as tar: diff --git a/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py b/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py index bb1bb481..4a18f291 100644 --- a/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py +++ b/mobsf/DynamicAnalyzer/views/ios/corellium_instance.py @@ -788,13 +788,11 @@ def download_data(request, bundle_id, api=False): dwd = Path(settings.DWD_DIR) pcap_file = dwd / f'{checksum}-network.pcap' pcap_file.write_bytes(pcap) - data = { - 'status': OK, - 'message': 'Downloaded application data', - } else: data['message'] = 'Failed to download pcap' - return send_response(data, api) + logger.error('Failed to download pcap') + data['status'] = OK + data['message'] = 'Downloaded application data' except Exception as exp: logger.exception('Downloading application data') data['message'] = str(exp) diff --git a/mobsf/DynamicAnalyzer/views/ios/device/connect.py b/mobsf/DynamicAnalyzer/views/ios/device/connect.py new file mode 100644 index 00000000..0e1878d4 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/connect.py @@ -0,0 +1,448 @@ +# -*- coding: utf_8 -*- +"""Connect to iOS device using SSH over USB or WiFi.""" + +import logging +import shutil +import socket +import subprocess +import threading +import time + +import paramiko + +from mobsf.DynamicAnalyzer.views.ios.helpers import ( + get_ios_model_mapping, +) + +logger = logging.getLogger(__name__) + + +class IOSConnector: + """Connect to iOS device using SSH over USB or WiFi.""" + + def __init__(self): + """Initialize the IOSConnector.""" + self.ssh_client = None + self.connection_type = None + self.device_info = {} + self.host = None + self.port = None + self.username = None + self.password = None + self.iproxy_procs = [] # Popen handles for iproxy subprocesses + + def _ssh_execute_command(self, command, timeout=30): + """Execute a command on SSH client.""" + if not self.ssh_client: + return None, None, None + + # Shared variables for thread communication + result = {'output': '', 'error': '', 'exit_status': + - 1, 'exception': None, 'channel': None} + command_completed = threading.Event() + + def execute_command(): + """Execute command in separate thread.""" + try: + # Execute command + _, stdout, stderr = self.ssh_client.exec_command(command) + channel = stdout.channel + result['channel'] = channel + + # Read output with partial capture + output_parts = [] + error_parts = [] + + # Read stdout in chunks to capture partial output + while True: + try: + chunk = stdout.read(1024).decode('utf-8', errors='ignore') + if not chunk: + break + output_parts.append(chunk) + except Exception: + break + + # Read stderr in chunks to capture partial output + while True: + try: + chunk = stderr.read(1024).decode('utf-8', errors='ignore') + if not chunk: + break + error_parts.append(chunk) + except Exception: + break + + result['output'] = ''.join(output_parts).strip() + result['error'] = ''.join(error_parts).strip() + + # Try to get exit status if available + try: + result['exit_status'] = channel.recv_exit_status() + except Exception: + result['exit_status'] = -1 + + except Exception as e: + result['exception'] = e + finally: + command_completed.set() + + # Start command execution in separate thread + command_thread = threading.Thread(target=execute_command, daemon=True) + command_thread.start() + + # Wait for command to complete or timeout + if command_completed.wait(timeout=timeout): + if result['exception']: + logger.error('Command execution failed: %s', str(result['exception'])) + return result['output'], result['error'], -1 + return result['output'], result['error'], result['exit_status'] + else: + if 'oslog' not in command: + logger.warning( + 'Command execution timed out after %d seconds: %s', + timeout, command) + + # Try to close the channel if it exists + if result['channel']: + try: + result['channel'].close() + except Exception: + pass + + # Return any partial output that was captured + return result['output'], result['error'], -1 + + def connect_usb(self, device_id, username, password, port=2222): + """Connect to iOS device using USB (via usbmuxd/iproxy).""" + try: + logger.info('Attempting USB connection to iOS device via SSH') + + # Check if usbmuxd/iproxy is available + if not self._check_usbmuxd(): + raise Exception( + 'usbmuxd/iproxy not available. Install libimobiledevice.') + + # Find USB connected iOS devices + devices = self.get_usb_devices() + if not devices: + raise Exception('No USB connected iOS devices found') + + # List all devices + logger.info('Available USB devices:') + for device in devices: + logger.info( + 'Device:\n' + ' ID: %s\n' + ' Name: %s\n' + ' Model: %s\n' + ' Version: %s\n' + ' Serial: %s', + device.get('id', 'Unknown'), + device.get('name', 'Unknown'), + device.get('model', 'Unknown'), + device.get('version', 'Unknown'), + device.get('serial', 'Unknown'), + ) + + # Use specified device or first available + if device_id: + device = next((d for d in devices if d['id'] == device_id), None) + if not device: + raise Exception('Device %s not found' % device_id) + else: + device = devices[0] + + logger.info( + 'Selected device:\n' + ' ID: %s\n' + ' Name: %s\n' + ' Model: %s\n' + ' Version: %s\n' + ' Serial: %s', + device.get('id', 'Unknown'), + device.get('name', 'Unknown'), + device.get('model', 'Unknown'), + device.get('version', 'Unknown'), + device.get('serial', 'Unknown'), + ) + + # Setup port forwarding using iproxy + self._setup_usb_port_forward(device['id'], port) + + # Prepare device info + device_info = { + 'id': device['id'], + 'name': device['name'], + 'type': 'usb', + 'connection': f'localhost:{port}', + } + + # Establish SSH connection + self._establish_ssh_connection( + 'localhost', port, username, password, 'usb', device_info) + + logger.info('Successfully connected to %s via USB SSH', device['name']) + return True + + except Exception as e: + logger.error('USB connection failed: %s', str(e)) + return False + + def connect_wifi(self, ip_address, username, password, port=22): + """Connect to iOS device using WiFi SSH.""" + try: + if port not in range(1, 65536): + raise Exception('Invalid port number') + logger.info('Attempting WiFi SSH connection to %s:%s', ip_address, port) + + # Prepare device info + device_info = { + 'ip': ip_address, + 'port': port, + 'type': 'wifi', + 'connection': f'{ip_address}:{port}', + } + + # Establish SSH connection + self._establish_ssh_connection( + ip_address, port, username, password, 'wifi', device_info) + return True + except Exception as e: + logger.error('WiFi connection failed: %s', str(e)) + return False + + def disconnect(self): + """Disconnect from iOS device.""" + try: + if self.ssh_client: + self.ssh_client.close() + self.ssh_client = None + + # Terminate only the iproxy processes started by this instance + for proc in self.iproxy_procs: + try: + proc.terminate() + except Exception: + pass + self.iproxy_procs = [] + logger.info('Disconnected from iOS device') + except Exception as e: + logger.error('Error during disconnect: %s', str(e)) + finally: + if hasattr(self, 'ssh_client') and self.ssh_client: + try: + self.ssh_client.close() + except Exception: + pass + self.ssh_client = None + self.connection_type = None + self.device_info = {} + self.host = None + self.port = None + self.username = None + self.password = None + + def get_usb_devices(self): + """Get list of USB connected iOS devices.""" + try: + logger.info('Getting iOS devices connected via USB') + if not shutil.which('idevice_id'): + logger.warning( + 'libimobiledevice is not installed. ' + 'Install it with: brew install libimobiledevice') + return [] + # Use idevice_id to list devices + result = subprocess.run(['idevice_id', '-l'], + capture_output=True, text=True) + if result.returncode != 0: + return [] + + devices = [] + model_mapping = get_ios_model_mapping() + for device_id in result.stdout.strip().split('\n'): + if device_id: + device_info = {} + # Get device details + output = subprocess.run(['ideviceinfo', '-u', device_id], + capture_output=True, text=True) + if output.returncode != 0: + continue + for line in output.stdout.splitlines(): + if ': ' in line: + key, value = line.split(': ', 1) + device_info[key.strip()] = value.strip() + + devices.append({ + 'id': device_id, + 'name': device_info.get('DeviceName'), + 'model': model_mapping.get( + device_info.get('ProductType'), + device_info.get('ProductType'), + ), + 'version': device_info.get('ProductVersion'), + 'serial': device_info.get('SerialNumber'), + }) + + return devices + except Exception as e: + logger.error('Failed to get USB devices: %s', str(e)) + return [] + + def _create_ssh_connection(self, host, port, username, password, timeout=10): + """Create and connect an SSH client.""" + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect( + hostname=host, + port=port, + username=username, + password=password, + timeout=timeout, + allow_agent=False, + look_for_keys=False, + ) + return ssh + + def _test_connection_ssh( + self, host, port, username, password, timeout=10): + """Test SSH connection to iOS device.""" + try: + logger.info('Testing SSH connection to %s:%s', host, port) + + # Create SSH client + ssh = self._create_ssh_connection(host, port, username, password, timeout) + + # Temporarily set self.ssh_client to test client + self.ssh_client = ssh + + # Test basic command execution - check user identity + output, _, _ = self._ssh_execute_command('id', timeout) + + # Check if we have root access + if 'uid=0(root)' in output: + logger.info('Verified iOS device is Jailbroken') + return True + + logger.warning('Not connected as root: %s', output) + ssh.close() + self.ssh_client = None + return False + + except Exception as e: + logger.error('SSH connection test failed: %s', str(e)) + if self.ssh_client: + try: + self.ssh_client.close() + except Exception: + pass + self.ssh_client = None + return False + + def _establish_ssh_connection( + self, host, port, username, password, connection_type, device_info): + """Establish SSH connection and set up instance variables.""" + # Test SSH connection first + if not self._test_connection_ssh(host, port, username, password): + raise Exception('SSH connection test failed') + + # Establish SSH connection + if not self.ssh_client: + self.ssh_client = self._create_ssh_connection( + host, port, username, password) + + # Set up instance variables + self.connection_type = connection_type + self.host = host + self.port = port + self.username = username + self.password = password + self.device_info = device_info + + logger.info('Successfully established SSH connection to %s:%s', host, port) + return True + + def _check_usbmuxd(self): + """Check if usbmuxd/iproxy is available.""" + try: + result = subprocess.run(['which', 'iproxy'], + capture_output=True, text=True) + return result.returncode == 0 + except Exception: + return False + + def _setup_usb_port_forward(self, device_id, port): + """Setup port forwarding for USB device.""" + try: + # Test if ports are open with separate sockets + def test_port(port_to_test): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(5) + result = sock.connect_ex(('localhost', port_to_test)) + sock.close() + return result == 0 + except Exception: + return False + + # Check if port forwarding already exists + ssh_port_open = test_port(port) + frida_port_open = test_port(37042) + + if ssh_port_open and frida_port_open: + logger.info( + 'Port forwarding already exists: ' + 'localhost:%s -> device:22, localhost:37042 -> device:27042', + port, + ) + return True + + # Port forwarding doesn't exist, so set it up + logger.info('Setting up new port forwarding for device %s', device_id) + # Kill any existing iproxy processes for this device (in case they're stuck) + subprocess.run(['pkill', '-f', f'iproxy.*{device_id}'], + capture_output=True) + time.sleep(1) + + # Start iproxy for port forwarding; store handles for clean teardown + cmds = [['iproxy', str(port), '22', '-u', device_id], + ['iproxy', '37042', '27042', '-u', device_id]] + for cmd in cmds: + logger.info('Starting iproxy: %s', ' '.join(cmd)) + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + self.iproxy_procs.append(proc) + + # Wait for port forwarding to be ready + logger.info('Waiting for port forwarding to be ready...') + time.sleep(2) # Give iproxy time to establish connections + + # Test if ports are now open + ssh_port_open = test_port(port) + frida_port_open = test_port(37042) + + if not ssh_port_open or not frida_port_open: + for proc in self.iproxy_procs: + try: + proc.terminate() + except Exception: + pass + self.iproxy_procs = [] + failed_ports = [] + if not ssh_port_open: + failed_ports.append(f'SSH port {port}') + if not frida_port_open: + failed_ports.append('Frida port 37042') + raise Exception( + f"Port forwarding failed for: {', '.join(failed_ports)}") + + logger.info( + 'Port forwarding established: ' + 'localhost:%s -> device:22, localhost:37042 -> device:27042', + port, + ) + return True + + except Exception as e: + logger.error('Failed to setup USB port forwarding: %s', str(e)) + return False diff --git a/mobsf/DynamicAnalyzer/views/ios/device/device.py b/mobsf/DynamicAnalyzer/views/ios/device/device.py new file mode 100644 index 00000000..1c991553 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/device.py @@ -0,0 +1,504 @@ +# -*- coding: utf_8 -*- +"""iOS device operations using SSH connection.""" + +import logging +import hashlib +import platform +import subprocess +import base64 +import tempfile +import shutil +import io +from pathlib import Path +import time +import plistlib + +from django.conf import settings + + +logger = logging.getLogger(__name__) + + +class IOSDevice: + """Handle iOS device operations using SSH connection.""" + + def __init__(self, connector): + """Initialize with an IOSConnector instance.""" + self.connector = connector + + def execute_command(self, command, timeout=30): + """Execute a command on SSH client.""" + if not self.connector.ssh_client: + return None, None, None + + return self.connector._ssh_execute_command(command, timeout) + + def ps(self): + """Get the list of running processes.""" + if not self.connector.ssh_client: + return None + output, error, exit_code = self.execute_command('ps aux') + if exit_code != 0: + logger.error('Failed to get list of running processes: %s', error) + return None + + # Parse ps aux output format + processes = [] + lines = output.split('\n') + + # Skip header line + for line in lines[1:]: + if line.strip(): + try: + # ps aux format: + # USER PID %CPU %MEM VSZ RSS TT STAT START TIME COMMAND + parts = line.split() + if len(parts) >= 11: + pid = parts[1] + command = ' '.join(parts[10:]) + + # Only include processes with .app in the path + if '.app/' in command: + # Extract app name from .app path + app_parts = command.split('.app/') + if app_parts: + app_path = app_parts[0] + app_name = app_path.split('/')[-1] + + processes.append({ + 'pid': pid, + 'process_name': app_name, + }) + except Exception as e: + logger.warning( + 'Failed to parse process line: %s - %s', line, str(e)) + continue + + return processes + + def get_cpu_architecture(self): + """Get the device CPU architecture.""" + if not self.connector.ssh_client: + return None + + try: + err = 'Failed to get device CPU architecture' + output, error, exit_code = self.execute_command('arch') + if exit_code != 0: + logger.error('%s: %s', err, error) + return None + arch = output.strip() + + arch_mapping = { + 'arm64': 'arm64', # iPhone 5s and later (A7+) + 'arm64e': 'arm64', # iPhone XS and later (A12+) + 'armv7': 'arm', # iPhone 4, 4S, 5, 5c + 'armv7s': 'arm', # iPhone 5, 5c (optimized for Swift) + } + return arch_mapping.get(arch) + + except Exception: + logger.exception(err) + return None + + def get_platform_architecture(self): + """Get the device platform architecture.""" + if not self.connector.ssh_client: + return None + + try: + err = 'Failed to get device platform architecture' + output, error, exit_code = self.execute_command('dpkg --print-architecture') + if exit_code != 0: + logger.error('%s: %s', err, error) + return None + arch = output.strip() + + if 'iphoneos-arm64' in arch: + return 'arm64' + return 'arm' + except Exception: + logger.exception(err) + return None + + def upload_file(self, local_path, remote_path): + """Upload a file to the iOS device.""" + if not self.connector.ssh_client: + return False + + try: + with self.connector.ssh_client.open_sftp() as sftp: + sftp.put(local_path, remote_path) + logger.info('Uploaded %s to %s', local_path, remote_path) + return True + except Exception: + logger.exception('Failed to upload file') + return False + + def upload_file_object(self, fobject, filename, path='/tmp/'): + """Upload a file object to the iOS device.""" + if not self.connector.ssh_client: + return False + try: + filename = Path(filename.replace('..', '')).name + path = Path(path.replace('..', '')) + remote_path = path / filename + with self.connector.ssh_client.open_sftp() as sftp: + sftp.putfo(fobject, str(remote_path)) + logger.info('Uploaded %s to %s', filename, str(remote_path)) + return True + except Exception: + logger.exception('Failed to upload file') + return False + + def download_file(self, remote_path, local_path): + """Download a file from the iOS device.""" + if not self.connector.ssh_client: + return False + try: + with self.connector.ssh_client.open_sftp() as sftp: + sftp.get(remote_path, local_path) + logger.info('Downloaded %s to %s', remote_path, local_path) + return True + except FileNotFoundError: + logger.error('File not found: %s', remote_path) + return False + except Exception: + logger.exception('Failed to download file') + return False + + def download_file_object(self, remote_path): + """Return a file object from the iOS device.""" + if not self.connector.ssh_client: + return False + try: + with self.connector.ssh_client.open_sftp() as sftp: + with io.BytesIO() as buffer: + sftp.getfo(remote_path, buffer) + buffer.seek(0) + return buffer.read() + except FileNotFoundError: + logger.error('File not found: %s', remote_path) + return False + except Exception: + logger.exception('Failed to download file') + return False + + def write_file(self, remote_path, content): + """Write a file to the iOS device.""" + if not self.connector.ssh_client: + return False + try: + with self.connector.ssh_client.open_sftp() as sftp: + with io.BytesIO(content.encode('utf-8')) as buffer: + sftp.putfo(buffer, remote_path) + return True + except Exception: + logger.exception('Failed to write file') + return False + + def read_file(self, remote_path): + """Read a text file from the iOS device.""" + if not self.connector.ssh_client: + return None + try: + with self.connector.ssh_client.open_sftp() as sftp: + with io.BytesIO() as buffer: + sftp.getfo(remote_path, buffer) + buffer.seek(0) + return buffer.read().decode('utf-8') + except Exception: + pass + return None + + def read_binary_file(self, remote_path): + """Read a binary file from the iOS device.""" + if not self.connector.ssh_client: + return None + try: + with self.connector.ssh_client.open_sftp() as sftp: + with io.BytesIO() as buffer: + sftp.getfo(remote_path, buffer) + buffer.seek(0) + return buffer.read() + except Exception: + logger.exception('Failed to read binary file') + return None + + def install_deb(self, remote_path): + """Install a deb file on the iOS device.""" + if not self.connector.ssh_client: + return False + try: + _, error, exit_code = self.execute_command(f'dpkg -i {remote_path}') + if exit_code != 0: + logger.error('Install failed in iOS device: %s', error) + return False + logger.info('Successfully installed deb file: %s', remote_path) + return True + except Exception: + logger.exception('Failed to install deb file') + return False + + def install_apt_package(self, package_name): + """Install an apt package on the iOS device.""" + if not self.connector.ssh_client: + return False + try: + _, error, exit_code = self.execute_command( + f'apt-get install -y {package_name}') + if exit_code != 0: + logger.error('Install failed in iOS device: %s', error) + return False + logger.info('Successfully installed apt package: %s', package_name) + return True + except Exception: + logger.exception('Failed to install apt package') + return False + + def _appsync_install(self): + """Check and install AppSync Unified. + + Returns: + False: Already installed - caller may proceed with IPA install. + True: Just installed and userspace reboot is in progress. + Caller should abort and retry after the reboot settles. + Raises: + Exception: AppSync could not be installed by either method. + """ + check_install = ( + r"apt list --installed | grep -E 'ai\.akemi\.app(inst|syncunified)'" + ) + out, _, _ = self.execute_command(check_install) + if 'appsyncunified' in out or 'appinst' in out: + return False # Already installed, proceed + + # AppSync is not installed - attempt installation + logger.info('AppSync Unified is not installed. Attempting to install...') + + # Method 1: Install from bundled deb file + tools_dir = Path(settings.TOOLS_DIR) / 'ios' / 'appsync' + if 'arm64' in self.get_platform_architecture(): + arm64 = ('ai.akemi.appsyncunified_116.0_iphoneos-arm64' + '.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb') + deb_file = tools_dir / arm64 + else: + arm = ('ai.akemi.appsyncunified_116.0_iphoneos-arm' + '.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb') + deb_file = tools_dir / arm + if not deb_file.exists(): + raise Exception('AppSync Unified deb file does not exist: %s' % deb_file) + remote_path = '/tmp/appsync.deb' + self.upload_file(deb_file, remote_path) + self.install_apt_package('mobilesubstrate') + if self.install_deb(remote_path): + logger.info( + 'AppSync Unified installed from bundled deb. ' + 'Rebooting userspace...') + self.execute_command('launchctl reboot userspace') + time.sleep(15) + return True + + # Method 2: Install from cydia.akemi.ai + logger.info('Attempting to install AppSync Unified from cydia.akemi.ai') + src_file = '/etc/apt/sources.list.d/cydia.list' + src = 'deb https://cydia.akemi.ai/ ./' + self.execute_command( + f'grep -qxF \'{src}\' {src_file} || echo \'{src}\' >> {src_file}') + self.execute_command('apt update') + _, error, exit_code = self.execute_command( + 'apt install -y --allow-unauthenticated ai.akemi.appinst') + if exit_code != 0: + raise Exception( + 'Failed to install AppSync Unified via apt: %s' % error) + logger.info( + 'AppSync Unified installed via apt. Rebooting userspace...') + self.execute_command('launchctl reboot userspace') + time.sleep(15) + return True + + def install_ipa(self, checksum): + """Install an IPA file on the iOS device.""" + if not self.connector.ssh_client: + return False + # Check if AppSync Unified is installed + if self._appsync_install(): + logger.info( + 'AppSync Unified was just installed and userspace is ' + 'rebooting. Please try again in a moment.') + return False + ipa_path = Path(settings.UPLD_DIR) / checksum / f'{checksum}.ipa' + if not ipa_path.exists(): + logger.error('IPA file does not exist: %s', ipa_path) + return False + if not self.upload_file(ipa_path, f'/tmp/{checksum}.ipa'): + logger.error('Failed to upload IPA file') + return False + out, error, exit_code = self.execute_command(f'appinst /tmp/{checksum}.ipa') + self.execute_command(f'rm -f /tmp/{checksum}.ipa') + if 'Successfully installed' in out: + logger.info('Successfully installed IPA') + return True + if exit_code != 0: + logger.error('Failed to install IPA: %s', error) + else: + logger.error('Failed to install IPA: %s', out) + return False + + def list_applications(self): + """List installed applications.""" + if not self.connector.ssh_client: + return [] + logger.info('Listing applications on iOS device') + + def classify(path): + return 'System' if path.startswith( + '/Applications/') else ( + 'User' if '/var/containers/Bundle/Application' in path else 'Other') + + def checksum(path): + return hashlib.md5(path.encode('utf-8')).hexdigest() + bundle_ids = [] + sftp_client = None + try: + sftp_client = self.connector.ssh_client.open_sftp() + output, _, exit_code = self.execute_command('uicache -l') + if exit_code == 0: + for line in output.split('\n'): + if line.strip(): + components = line.split(' : ') + bundle_id = components[0].strip() + app_path = components[1].strip() + app_name, app_icon = self.get_app_name(app_path, sftp_client) + bundle_ids.append({ + 'app_name': app_name, + 'app_icon': self.get_app_icon( + app_path, app_icon, sftp_client), + 'bundle_id': bundle_id, + 'app_path': app_path, + 'app_type': classify(app_path), + }) + sftp_client.close() + return bundle_ids + else: + logger.error('Failed to get app details') + return [] + except Exception: + logger.exception('Failed to get app details') + return [] + finally: + if sftp_client: + sftp_client.close() + + def get_app_name(self, app_path, sftp): + """Get name of an app.""" + if not self.connector.ssh_client: + return None + try: + plist_path = Path(app_path) / 'Info.plist' + with io.BytesIO() as buffer: + sftp.getfo(str(plist_path), buffer) + buffer.seek(0) + plist = plistlib.load(buffer) + app_name = ( + plist.get('CFBundleDisplayName') + or plist.get('CFBundleName') + or plist.get('CFBundleExecutable') + or 'Unknown App' + ) + app_icon = None + # First try modern nested structure + try: + icons = (plist['CFBundleIcons']['CFBundlePrimaryIcon'] + ['CFBundleIconFiles']) + if isinstance(icons, list) and icons: + app_icon = icons[-1] # Return the largest/resolution version + except (KeyError, TypeError): + pass + if not app_icon: + # Fallback to legacy key + app_icon = plist.get('CFBundleIconFile', None) + return app_name, app_icon + except Exception: + logger.exception('Failed to get app name') + return None, None + + def _crush_png(self, icon_path): + """Crush a png file.""" + try: + tools_dir = Path(settings.BASE_DIR) / 'StaticAnalyzer' / 'tools' / 'ios' + arch = platform.machine() + system = platform.system() + # Uncrush PNG. CgBI -> PNG + # https://iphonedevwiki.net/index.php/CgBI_file_format + if system == 'Darwin': + args = ['xcrun', '-sdk', 'iphoneos', 'pngcrush', '-q', + '-revert-iphone-optimizations', + icon_path, icon_path + '.fixed'] + try: + out = subprocess.run(args, capture_output=True) + if b'libpng error:' in out.stdout: + # PNG looks normal + raise ValueError('PNG is not CgBI') + shutil.move(icon_path + '.fixed', icon_path) + except Exception: + pass + else: + # Windows/Linux + cgbipng_bin = None + if system == 'Windows' and arch in ('AMD64', 'x86'): + cgbipng_bin = 'CgbiPngFix.exe' + elif system == 'Linux' and arch == 'x86_64': + cgbipng_bin = 'CgbiPngFix_amd64' + elif system == 'Linux' and arch == 'aarch64': + cgbipng_bin = 'CgbiPngFix_arm64' + if cgbipng_bin: + cbin = tools_dir / 'CgbiPngFix' / cgbipng_bin + args = [cbin.as_posix(), '-i', + icon_path, '-o', icon_path + '.fixed'] + try: + out = subprocess.run(args, capture_output=True) + shutil.move(icon_path + '.fixed', icon_path) + except Exception: + # Fails or PNG is not crushed + pass + else: + logger.warning('CgbiPngFix not available for %s %s', system, arch) + except Exception: + logger.exception('Failed to crush png') + return None + + def get_app_icon(self, app_path, app_icon, sftp): + """Get app icon.""" + if not self.connector.ssh_client: + return None + temp_path = None + try: + if app_icon: + # Use the provided icon name from Info.plist + search_pattern = f'{app_icon}*.png' + else: + # Guess icon path + search_pattern = 'AppIcon*.png' + output, _, exit_code = self.execute_command( + f'find "{app_path}" -iname "{search_pattern}"', + ) + if exit_code == 0: + if '.png' not in output: + return None + for line in output.split('\n'): + if line.strip(): + icon_path = Path(line.strip()) + with tempfile.NamedTemporaryFile( + suffix='.png', delete=False) as temp_file: + temp_path = Path(temp_file.name) + sftp.get(str(icon_path), str(temp_path)) + self._crush_png(str(temp_path)) + return base64.b64encode( + temp_path.read_bytes()).decode('utf-8') + except Exception: + logger.exception('Failed to get app icon') + finally: + if temp_path and temp_path.exists(): + temp_path.unlink() + return None diff --git a/mobsf/DynamicAnalyzer/views/ios/device/dynamic_analyzer.py b/mobsf/DynamicAnalyzer/views/ios/device/dynamic_analyzer.py new file mode 100644 index 00000000..9dd03d98 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/dynamic_analyzer.py @@ -0,0 +1,508 @@ +# -*- coding: utf_8 -*- +"""iOS Dynamic Analysis.""" +import logging +import os +import time +from threading import Event, Thread +from pathlib import Path + +from django.conf import settings +from django.shortcuts import render +from django.views.decorators.http import require_http_methods + + +from mobsf.MobSF.utils import ( + IOS_DEVICE_ID_REGEX, + SSH_DEVICE_ID_REGEX, + get_md5, + is_md5, + parse_host_port, + print_n_send_error_response, + strict_package_check, +) +from mobsf.DynamicAnalyzer.views.common.shared import ( + invalid_params, + is_attack_pattern, + send_response, +) +from mobsf.DynamicAnalyzer.forms import UploadFileForm +from mobsf.DynamicAnalyzer.views.ios.helpers import ( + configure_proxy, + get_local_ipa_list, +) +from mobsf.DynamicAnalyzer.views.ios.device.device import IOSDevice +from mobsf.DynamicAnalyzer.views.ios.device.connect import IOSConnector +from mobsf.DynamicAnalyzer.views.ios.device.environment import IOSEnvironment +from mobsf.DynamicAnalyzer.views.ios.device.frida import FridaIOSDevice +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) + +logger = logging.getLogger(__name__) +OK = 'ok' +FAILED = 'failed' + + +@login_required +@permission_required(Permissions.SCAN) +def dynamic_analysis_device(request, api=False): + """The iOS Device Dynamic Analysis Entry point.""" + try: + if api: + device_id = request.POST.get('device_id', '') + else: + device_id = request.GET.get('device_id', '') + scan_apps = get_local_ipa_list() + connector = IOSConnector() + wifi_devices = get_ios_devices_over_wifi() + if os.getenv('MOBSF_PLATFORM') == 'docker': + devices = [] + else: + devices = connector.get_usb_devices() + context = { + 'selected_device': device_id, + 'apps': scan_apps, + 'usb_devices': devices, + 'wifi_devices': wifi_devices, + 'title': 'MobSF Dynamic Analysis Jailbroken Device', + 'version': settings.MOBSF_VER, + } + if api: + return context + template = 'dynamic_analysis/ios/device/dynamic_analysis.html' + return render(request, template, context) + except Exception as exp: + logger.exception('iOS Jailbroken Device Dynamic Analysis') + return print_n_send_error_response(request, exp, api) + + +@login_required +@permission_required(Permissions.SCAN) +def dynamic_analyzer_device(request, api=False): + """Dynamic Analyzer for Jailbroken iOS devices.""" + try: + if api: + bundleid = request.POST.get('bundle_id') + device_id = request.POST.get('device_id') + form = None + else: + bundleid = request.GET.get('bundle_id') + device_id = request.GET.get('device_id') + form = UploadFileForm() + if not bundleid or not strict_package_check(bundleid): + return print_n_send_error_response( + request, + 'Invalid iOS Bundle id', + api) + checksum = get_md5(bundleid.encode('utf-8')) + ios_device, error = validate_and_connect_device(device_id) + if error: + return print_n_send_error_response( + request, + error, + api) + + app_dir = Path(settings.UPLD_DIR) / checksum + if not app_dir.exists(): + app_dir.mkdir() + configure_proxy(request, bundleid, None) + env = IOSEnvironment(ios_device) + env.setup_or_start_frida() + context = { + 'checksum': checksum, + 'device_id': device_id, + 'bundle_id': bundleid, + 'version': settings.MOBSF_VER, + 'form': form, + 'title': 'iOS Device Dynamic Analyzer'} + template = 'dynamic_analysis/ios/device/dynamic_analyzer.html' + if api: + return context + return render(request, template, context) + except Exception: + logger.exception('iOS Device Dynamic Analyzer') + return print_n_send_error_response( + request, + 'iOS Device Dynamic Analysis Failed.', + api) + + +# JSON API +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def ios_instrument_device(request, api=False): + """Instrument app with frida.""" + data = { + 'status': FAILED, + 'message': ''} + try: + action = request.POST.get('frida_action', 'spawn') + pid = request.POST.get('pid') + new_bundle_id = request.POST.get('new_bundle_id') + device_id = request.POST['device_id'] + bundle_id = request.POST['bundle_id'] + md5_hash = get_md5(bundle_id.encode('utf-8')) + default_hooks = request.POST['default_hooks'] + dump_hooks = request.POST['dump_hooks'] + auxiliary_hooks = request.POST['auxiliary_hooks'] + code = request.POST['frida_code'] + + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + if not strict_package_check(bundle_id): + data['message'] = 'Invalid iOS Bundle id' + return send_response(data, api) + if new_bundle_id and not strict_package_check(new_bundle_id): + data['message'] = 'Invalid iOS Bundle id' + return send_response(data, api) + + # Fill extras + extras = {} + screenshot_action = request.POST.get('screenshot_action') + if screenshot_action: + extras['screenshot_action'] = screenshot_action.strip() + class_name = request.POST.get('class_name') + if class_name: + extras['class_name'] = class_name.strip() + class_search = request.POST.get('class_search') + if class_search: + extras['class_search'] = class_search.strip() + method_search = request.POST.get('method_search') + if method_search: + extras['method_search'] = method_search.strip() + cls_trace = request.POST.get('class_trace') + if cls_trace: + extras['class_trace'] = cls_trace.strip() + if (is_attack_pattern(default_hooks) + or is_attack_pattern(dump_hooks) + or is_attack_pattern(auxiliary_hooks) + or not is_md5(md5_hash)): + return invalid_params(api) + frida_obj = FridaIOSDevice( + ios_device, + None, + md5_hash, + bundle_id, + default_hooks.split(','), + dump_hooks.split(','), + auxiliary_hooks.split(','), + extras, + code) + if action == 'run': + frida_obj.run_app() + if action == 'spawn': + logger.info('Starting Instrumentation') + frida_obj.spawn() + elif action == 'ps': + logger.info('Enumerating running applications') + data['message'] = frida_obj.ps() + elif action == 'get': + # Get injected Frida script. + data['message'] = frida_obj.get_script(nolog=True) + elif action == 'capture': + # Capture screenshot + screenshot_event = Event() + frida_obj.screenshot_event = screenshot_event + Thread( + target=download_screenshot, + args=(ios_device, md5_hash, screenshot_event), + daemon=True).start() + + if action in ('spawn', 'session', 'capture'): + if pid and pid.isdigit(): + # Attach to a different pid/bundle id + args = (int(pid), new_bundle_id) + logger.info('Attaching to %s [PID: %s]', new_bundle_id, pid) + else: + # Injecting to existing session/spawn + if action == 'session': + logger.info('Injecting to existing frida session') + args = (None, None) + Thread(target=frida_obj.session, args=args, daemon=True).start() + data['status'] = OK + except Exception as exp: + logger.exception('Frida Instrumentation failed') + data['message'] = str(exp) + return send_response(data, api) + + +# JSON API +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def ssh_execute_device(request, api=False): + """Execute commands in iOS device over SSH.""" + data = { + 'status': FAILED, + 'message': 'Failed to execute command'} + try: + device_id = request.POST['device_id'] + cmd = request.POST['cmd'] + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + out, err, _ = ios_device.execute_command(cmd) + message = f'{out}\n{err}' + data['status'] = OK + data['message'] = message + except Exception as exp: + data['message'] = str(exp) + logger.exception('Executing Commands in iOS device') + return send_response(data, api) + + +# JSON API +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def upload_file_device(request, api=False): + """Upload file to device.""" + err_msg = 'Failed to upload file' + data = { + 'status': FAILED, + 'message': err_msg, + } + try: + device_id = request.POST['device_id'] + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + form = UploadFileForm(request.POST, request.FILES) + if form.is_valid(): + fobject = request.FILES['file'] + upload = ios_device.upload_file_object( + fobject, fobject.name) + if upload: + data['status'] = OK + data['message'] = 'Successfully uploaded file' + else: + data['message'] = 'Failed to upload file' + except Exception as exp: + logger.exception(err_msg) + data['message'] = str(exp) + return send_response(data, api) + + +# JSON API +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST', 'GET']) +def system_logs_device(request, api=False): + """Show system logs.""" + data = { + 'status': FAILED, + 'message': 'Failed to get system logs', + } + try: + pid = request.GET.get('pid') + if request.method == 'POST': + device_id = request.POST['device_id'] + if pid and not pid.isdigit(): + data['message'] = 'Invalid PID' + return send_response(data, api) + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + cmd = 'oslog' + if pid: + cmd = f'oslog -p {pid}' + out, err, _ = ios_device.execute_command(cmd, timeout=9) + message = f'{out}\n{err}' + data['status'] = OK + data['message'] = message + return send_response(data, api) + # GET request + logger.info('Getting logs') + device_id = request.GET['device_id'] + if pid and not pid.isdigit(): + data['message'] = 'Invalid PID' + return send_response(data, api) + usb = IOS_DEVICE_ID_REGEX.match(device_id) + wifi = SSH_DEVICE_ID_REGEX.match(device_id) + if not (usb or wifi): + data['message'] = 'Invalid device id or SSH connection string format' + return send_response(data, api) + template = 'dynamic_analysis/ios/device/system_logs.html' + return render(request, + template, + {'device_id': device_id, + 'pid': pid, + 'version': settings.MOBSF_VER, + 'title': 'Live logs'}) + except Exception as exp: + logger.exception('Getting logs') + data['message'] = str(exp) + return send_response(data, api) + + +# JSON API +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def ps_device(request, api=False): + """Get list of running processes on iOS device.""" + data = { + 'status': FAILED, + 'message': 'Failed to get process list', + } + try: + device_id = request.POST.get('device_id', '') + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + processes = ios_device.ps() + if processes: + data['status'] = OK + data['message'] = processes + else: + data['message'] = 'No processes found' + + except Exception as exp: + logger.exception('Getting process list failed') + data['message'] = str(exp) + return send_response(data, api) + + +# JSON API +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def get_ios_device(request, api=False): + """Get details of selected iOS device.""" + data = { + 'status': 'failed', + 'message': 'Failed to connect to iOS device', + } + try: + device_id = request.POST.get('device_id', '') + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + data['apps'] = ios_device.list_applications() + data['status'] = OK + data['message'] = 'Successfully connected to iOS device' + except Exception: + logger.exception('Failed to connect to iOS device') + return send_response(data, api) + + +# JSON API +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def install_ipa_device(request, api=False): + """Install an IPA file on the device.""" + data = { + 'status': 'failed', + 'message': 'Failed to install IPA on device', + } + try: + device_id = request.POST.get('device_id', '') + # Checksum from IPA SAST Upload location + checksum = request.POST.get('checksum', '') + if not is_md5(checksum): + data['message'] = 'Invalid checksum format' + return send_response(data, api) + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + if not ios_device.install_ipa(checksum): + data['message'] = 'Failed to install IPA' + return send_response(data, api) + data['status'] = OK + data['message'] = 'Successfully installed IPA' + except Exception: + logger.exception('Failed to install IPA on iOS device') + return send_response(data, api) + + +# Helpers +def get_ios_devices_over_wifi(): + """Get available iOS devices over WiFi.""" + devices = [] + connect_strings = None + connect_strings_settings = getattr(settings, 'IOS_ANALYZER_IDENTIFIERS', '') + if os.getenv('MOBSF_IOS_ANALYZER_IDENTIFIERS'): + connect_strings = os.getenv('MOBSF_IOS_ANALYZER_IDENTIFIERS') + elif connect_strings_settings: + connect_strings = connect_strings_settings + try: + if not connect_strings: + return devices + if ',' in connect_strings: + # Multiple devices + for device in connect_strings.split(','): + devices.append(parse_host_port(device.strip())) + else: + # Single device + devices.append(parse_host_port(connect_strings.strip())) + except ValueError: + pass + return devices + + +def get_ios_device_wifi_connect_string(ssh_string): + """Get iOS device connect string.""" + available_devices = get_ios_devices_over_wifi() + if not ssh_string: + return None, None + ip, port = parse_host_port(ssh_string) + if (ip, port) in available_devices: + return ip, port + return None, None + + +def download_screenshot(ios_device, checksum, screenshot_event=None): + """Download screenshot from device.""" + try: + # Wait for the Frida script to signal screenshot completion + # rather than using a fixed sleep, with a 30s timeout as fallback. + if screenshot_event: + screenshot_event.wait(timeout=30) + else: + time.sleep(5) + timestamp = time.strftime('%Y%m%d%H%M%S') + path = Path(settings.DWD_DIR) / f'{checksum}-sshot-{timestamp}.png' + ios_device.download_file('/tmp/screenshot.png', path) + except Exception as exp: + logger.error('Failed to download screenshot from iOS device') + logger.error(exp) + + +def validate_and_connect_device(device_id): + """Validate device_id and establish connection to iOS device.""" + usb = IOS_DEVICE_ID_REGEX.match(device_id) + wifi = SSH_DEVICE_ID_REGEX.match(device_id) + if not (usb or wifi): + return None, 'Invalid device id or SSH connection string format' + + ssh_user = getattr(settings, 'IOS_SSH_USER', 'root') + ssh_password = getattr(settings, 'IOS_SSH_PASSWORD', 'alpine') + + connector = IOSConnector() + if usb: + connected = connector.connect_usb( + device_id, username=ssh_user, password=ssh_password) + if not connected: + return None, 'Failed to connect to iOS device over USB' + else: + ip, port = get_ios_device_wifi_connect_string(device_id) + connected = connector.connect_wifi( + ip, ssh_user, ssh_password, port=port) + if not connected: + return None, 'Failed to connect to iOS device over WiFi' + ios_device = IOSDevice(connector) + return ios_device, None diff --git a/mobsf/DynamicAnalyzer/views/ios/device/environment.py b/mobsf/DynamicAnalyzer/views/ios/device/environment.py new file mode 100644 index 00000000..d2c2d301 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/environment.py @@ -0,0 +1,319 @@ +# -*- coding: utf_8 -*- +"""iOS Device Environment for Dynamic Analysis.""" + +import io +import logging +import plistlib +import time +from pathlib import Path + +from django.conf import settings + +from frida import __version__ as frida_version + +from mobsf.DynamicAnalyzer.views.common.frida.server_update import ( + FridaServerUpdater, +) + +logger = logging.getLogger(__name__) + + +class IOSEnvironment: + """iOS Device Environment for Dynamic Analysis.""" + + def __init__(self, ios_device): + """Initialize iOS Environment with device operations. + + Args: + ios_device: IOSDevice instance + """ + self.ios_device = ios_device + self.connector = ios_device.connector + self.frida_server_path = None + self.frida_process = None + self.frida_plist_path = None + self.platform = None + self.platform_architecture = self.ios_device.get_platform_architecture() + + def _find_frida_server_plist(self): + """Find Frida server plist file on iOS device.""" + if self.frida_plist_path: + return self.frida_plist_path + + try: + # Common locations where Frida server plist might be installed + plist_locations = [ + '/Library/LaunchDaemons/re.frida.server.plist', + '/var/jb/Library/LaunchDaemons/re.frida.server.plist', + '/System/Library/LaunchDaemons/re.frida.server.plist', + ] + + for plist_path in plist_locations: + output, _, exit_code = self.ios_device.execute_command( + f'ls -la {plist_path}', + ) + + if exit_code == 0 and 'No such file' not in output: + self.frida_plist_path = plist_path + return plist_path + + # If not found in common locations, search for it + logger.info('Searching for Frida server plist files...') + output, _, exit_code = self.ios_device.execute_command( + 'find / -name "*frida*server*.plist" 2>/dev/null', + ) + + if exit_code == 0 and output.strip(): + plist_files = output.strip().split('\n') + for plist_file in plist_files: + if plist_file.strip(): + self.frida_plist_path = plist_file.strip() + return plist_file.strip() + + logger.warning('No Frida server plist file found') + return None + + except Exception: + logger.exception('[ERROR] Finding Frida server plist') + return None + + def _install_frida_server_on_device(self): + """Install Frida server on iOS device.""" + try: + logger.info('Installing Frida Server v%s on iOS device', frida_version) + + # Upload the binary to the device + success = self.ios_device.upload_file( + self.frida_server_path, + '/tmp/fd.deb', + ) + + if not success: + raise Exception( + 'Failed to upload Frida server v%s binary to device', frida_version) + + success = self.ios_device.install_deb('/tmp/fd.deb') + if not success: + raise Exception( + 'Failed to install Frida server v%s on device', frida_version) + return True + + except Exception: + logger.exception( + '[ERROR] Installing Frida server v%s on iOS device', frida_version) + return False + + def _modify_frida_plist_for_wifi(self): + """Modify Frida server plist to enable WiFi access using plistlib.""" + try: + self._find_frida_server_plist() + if not self.frida_plist_path: + logger.error('No Frida plist path available') + return False + + logger.info('Modifying Frida plist for WiFi access') + # Read as raw bytes — handles both XML and binary plist formats. + # read_file() would fail on binary plists due to UTF-8 decoding. + plist_bytes = self.ios_device.read_binary_file(self.frida_plist_path) + if not plist_bytes: + raise Exception('Failed to read Frida plist') + + # Quick check for already-patched XML plist (avoids re-parsing) + if (b'-l' in plist_bytes + and b'0.0.0.0' in plist_bytes): + logger.info('WiFi arguments already present in plist') + return True + + # plistlib.loads() handles both XML and binary plist formats + plist = plistlib.loads(plist_bytes) + args = plist.get('ProgramArguments', []) + if '-l' not in args: + args.append('-l') + if '0.0.0.0' not in args: + args.append('0.0.0.0') + plist['ProgramArguments'] = args + logger.info('Added -l 0.0.0.0 to ProgramArguments') + + # Serialize back to XML and upload in-memory + updated_bytes = plistlib.dumps(plist, fmt=plistlib.FMT_XML) + plist_name = Path(self.frida_plist_path).name + plist_dir = str(Path(self.frida_plist_path).parent) + '/' + self.ios_device.upload_file_object( + io.BytesIO(updated_bytes), plist_name, plist_dir) + + logger.info('Successfully modified Frida plist for WiFi access') + return True + + except Exception: + logger.exception('[ERROR] Modifying Frida plist for WiFi access') + return False + + def _enable_frida_on_wifi(self): + """Enable Frida on WiFi.""" + try: + logger.info('Enabling Frida Server v%s on 0.0.0.0', frida_version) + self._find_frida_server_plist() + if not self.frida_plist_path: + raise ValueError('Frida server plist not found') + + # Modify the plist to add WiFi arguments + if not self._modify_frida_plist_for_wifi(): + raise ValueError('Failed to modify plist for WiFi access') + + # Reload the modified plist + self.start_frida_server() + logger.info( + 'Successfully started Frida Server v%s on (0.0.0.0)', frida_version) + return True + + except Exception: + logger.exception( + '[ERROR] Enabling Frida Server v%s on 0.0.0.0', frida_version) + return False + + def setup_or_start_frida(self): + """Setup and start Frida server for iOS device.""" + try: + # Check if setup is already done + if self.ios_device.read_file('/tmp/frida_setup_done') == frida_version: + self.start_frida_server() + logger.info( + 'Successfully started Frida Server v%s on (0.0.0.0)', frida_version) + return True + + # Get iOS platform architecture + if not self.platform_architecture: + raise ValueError('Failed to determine iOS platform architecture') + + logger.info('iOS platform architecture identified as %s', + self.platform_architecture) + + # Update/download Frida server + platform = 'iphoneos' + frida_bin = ( + f'frida_{frida_version}_{platform}-{self.platform_architecture}.deb') + self.frida_server_path = Path(settings.DWD_DIR) / frida_bin + if not self.frida_server_path.is_file(): + # Download Frida server binary if not found + updater = FridaServerUpdater(platform, frida_version) + success = updater.update_frida_server( + self.platform_architecture, + ) + if not success: + msg = ( + 'Cannot download frida binary for iOS. You will need' + f' {frida_bin} in {settings.DWD_DIR}' + ' for Dynamic Analysis to work') + raise ValueError(msg) + + # install frida server on device + self._install_frida_server_on_device() + + # Enable and start Frida on WiFi + self._enable_frida_on_wifi() + + # Install oslog on device + self.install_oslog() + + # Create a file to indicate that frida setup is done + self.ios_device.write_file('/tmp/frida_setup_done', frida_version) + return True + except Exception: + logger.exception('[ERROR] Setting up Frida for iOS') + return False + + def start_frida_server(self): + """Start Frida Server on iOS device using launchctl.""" + try: + self.stop_frida_server() + # Load and start the Frida server using launchctl + output, _, exit_code = self.ios_device.execute_command( + f'launchctl load {self.frida_plist_path}', + ) + + if exit_code != 0: + raise ValueError('Failed to load Frida server plist') + + # Wait for the server to start + logger.info('Waiting for Frida server to start...') + + # Verify the server is running + output, _, exit_code = self.ios_device.execute_command( + 'ps aux | grep frida-server | grep -v grep', + ) + + if exit_code != 0: + # Attempt to start the server using the binary + _, error, exit_code = self.ios_device.execute_command( + 'frida-server -l 0.0.0.0 &', + ) + if exit_code != 0: + raise ValueError( + f'Failed to start Frida server using binary: {error}', + ) + output, _, exit_code = self.ios_device.execute_command( + 'ps aux | grep frida-server | grep -v grep', + ) + time.sleep(1) + if exit_code == 0 and output.strip(): + logger.info('Frida Server started successfully') + return True + else: + raise Exception('Frida Server failed to start') + + except Exception: + logger.exception('[ERROR] Running Frida server on iOS device') + return False + + def stop_frida_server(self): + """Stop Frida server on iOS device using launchctl.""" + try: + self._find_frida_server_plist() + if not self.frida_plist_path: + raise Exception('Frida server plist not found') + + # Unload the plist using launchctl + output, error, exit_code = self.ios_device.execute_command( + f'launchctl unload {self.frida_plist_path}', + ) + + if exit_code == 0: + logger.info('Successfully stopped Frida server') + else: + logger.warning('Failed to stop Frida server: %s', error) + + # Also kill any remaining Frida server processes as fallback + _, error, exit_code = self.ios_device.execute_command( + 'pkill -f frida-server', + ) + + if exit_code == 0: + logger.info('Killed remaining Frida server processes') + + return True + except Exception: + logger.exception('[ERROR] Stopping Frida server') + return False + + def install_oslog(self): + """Install oslog on iOS device.""" + try: + logger.info('Installing oslog on iOS device') + if not self.platform_architecture: + raise ValueError('Failed to determine iOS platform architecture') + tools_dir = Path(settings.TOOLS_DIR) / 'ios' / 'oslog' + if self.platform_architecture == 'arm64': + oslog_bin = 'oslog_0.0.4.3_iphoneos-arm64.deb' + else: + oslog_bin = 'oslog_0.0.1-8_iphoneos-arm.deb' + deb_file = tools_dir / oslog_bin + if not deb_file.exists(): + raise ValueError('oslog deb file does not exist: %s', deb_file) + remote_path = '/tmp/oslog.deb' + self.ios_device.upload_file(deb_file, remote_path) + if not self.ios_device.install_deb(remote_path): + raise ValueError('Failed to install oslog on device') + return True + except Exception: + logger.exception('[ERROR] Installing oslog on iOS device') + return False diff --git a/mobsf/DynamicAnalyzer/views/ios/device/frida.py b/mobsf/DynamicAnalyzer/views/ios/device/frida.py new file mode 100644 index 00000000..f56e7130 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/frida.py @@ -0,0 +1,179 @@ +"""Frida Core Class for iOS Device.""" +import logging +import time +import sys + +import frida + +from mobsf.DynamicAnalyzer.views.ios.frida_core import Frida +from mobsf.DynamicAnalyzer.views.ios.device.environment import IOSEnvironment + +logger = logging.getLogger(__name__) +_PID = None +_SPAWN_LOG_MSG = 'Spawned %s with PID %s' + + +class FridaIOSDevice(Frida): + """Frida iOS Device for Dynamic Analysis.""" + + def __init__( + self, ios_device, ssh_string, app_hash, bundle_id, + defaults, dump, auxiliary, extras, code): + """Initialize.""" + super().__init__( + ssh_string, app_hash, bundle_id, defaults, dump, auxiliary, extras, code) + self.connector = ios_device.connector + self.env = IOSEnvironment(ios_device) + self.api = None + # USB path: iproxy maps localhost:37042 -> device:27042 + # MobSF starts this automatically via _setup_usb_port_forward(). + # Frida connects to localhost:37042. + # + # WiFi / Docker path: Frida connects directly to the device host + # on the default Frida port (27042). No local port mapping is + # created by MobSF. When running in Docker/Colima, run iproxy + # on the macOS host before starting MobSF: + # iproxy -s 0.0.0.0 2222:22 27042:27042 -u + # Then set MOBSF_IOS_ANALYZER_IDENTIFIERS=host.lima.internal:2222 + # (Colima) or host.docker.internal:2222 (Docker Desktop). + if self.connector.connection_type == 'usb': + self.frida_device = frida.get_device_manager().add_remote_device( + f'{self.connector.host}:37042') + else: + self.frida_device = frida.get_device_manager().add_remote_device( + self.connector.host) + + def run_app(self): + """Run the app with frida.""" + pid = None + try: + try: + pid = self.frida_device.spawn([self.bundle_id]) + self.frida_device.resume(pid) + logger.info(_SPAWN_LOG_MSG, self.bundle_id, pid) + return pid + except frida.NotSupportedError: + logger.error(self.not_supported_text) + logger.info(_SPAWN_LOG_MSG, self.bundle_id, pid) + return pid + except frida.ServerNotRunningError: + self.env.start_frida_server() + time.sleep(1) + pid = self.frida_device.spawn([self.bundle_id]) + self.frida_device.resume(pid) + logger.info(_SPAWN_LOG_MSG, self.bundle_id, pid) + return pid + except frida.TimedOutError: + logger.error('Timed out while waiting for device to appear') + except frida.ServerNotRunningError: + logger.error('Frida Server is not running') + except frida.NotSupportedError: + logger.error(self.not_supported_text) + except (frida.ProcessNotFoundError, + frida.ProcessNotRespondingError, + frida.TransportError, + frida.InvalidOperationError): + pass + except Exception: + logger.exception('Failed to run app') + return None + + def spawn(self): + """Connect to Frida Server and spawn the app.""" + global _PID + try: + try: + self.clean_up() + _PID = self.frida_device.spawn([self.bundle_id]) + except frida.NotSupportedError: + logger.error(self.not_supported_text) + return + except frida.ServerNotRunningError: + self.env.start_frida_server() + _PID = None + if not _PID: + _PID = self.frida_device.spawn([self.bundle_id]) + logger.info(_SPAWN_LOG_MSG, self.bundle_id, _PID) + except frida.TimedOutError: + logger.error('Timed out while waiting for device to appear') + except frida.ServerNotRunningError: + logger.error('Frida Server is not running') + except frida.NotSupportedError: + logger.error(self.not_supported_text) + return + except (frida.ProcessNotFoundError, + frida.ProcessNotRespondingError, + frida.TransportError, + frida.InvalidOperationError): + pass + except Exception: + logger.exception('Error Connecting to Frida Server') + + def session(self, pid, bundle_id): + """Use existing session to inject frida scripts.""" + global _PID + try: + try: + if pid and bundle_id: + _PID = pid + self.bundle_id = bundle_id + try: + front = self.frida_device.get_frontmost_application() + if front or front.pid != _PID: + # Not the front most app. + # Get the pid of the front most app + logger.warning('Front most app has PID %s', front.pid) + _PID = front.pid + except Exception: + pass + self.frida_device.resume(_PID) + time.sleep(2) + session = self.frida_device.attach(_PID) + except frida.NotSupportedError: + logger.error(self.not_supported_text) + return + except Exception: + logger.warning('Cannot attach to pid, spawning again.') + self.spawn() + self.frida_device.resume(_PID) + time.sleep(2) + session = self.frida_device.attach(_PID) + if session and self.frida_device and _PID: + script = session.create_script(self.get_script()) + script.on('message', self.frida_response) + script.load() + self.api = script.exports_sync + self.api_handler(self.api) + sys.stdin.read() + script.unload() + session.detach() + except frida.NotSupportedError: + logger.error(self.not_supported_text) + except (frida.ProcessNotFoundError, + frida.ProcessNotRespondingError, + frida.TransportError, + frida.InvalidOperationError): + pass + except Exception: + logger.exception('Error Connecting to Frida Server') + + def ps(self): + """Get running process pid.""" + ps_dict = [] + try: + try: + processes = self.frida_device.enumerate_applications(scope='minimal') + except frida.ServerNotRunningError: + self.env.start_frida_server() + processes = self.frida_device.enumerate_applications(scope='minimal') + if self.frida_device and processes: + for process in processes: + if process.pid != 0: + ps_dict.append({ + 'pid': process.pid, + 'name': process.name, + 'identifier': process.identifier, + }) + except Exception: + logger.exception('Failed to enumerate running applications') + return ps_dict diff --git a/mobsf/DynamicAnalyzer/views/ios/device/report.py b/mobsf/DynamicAnalyzer/views/ios/device/report.py new file mode 100644 index 00000000..43b5fa50 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/report.py @@ -0,0 +1,312 @@ +# -*- coding: utf_8 -*- +"""Dynamic Analyzer Reporting for iOS devices.""" +import logging +import shutil +from pathlib import Path +import json + +from django.conf import settings +from django.shortcuts import render +from django.template.defaulttags import register +from django.views.decorators.http import require_http_methods +from django.http import HttpResponse + +import mobsf.MalwareAnalyzer.views.Trackers as Trackers +from mobsf.DynamicAnalyzer.views.ios.analysis import ( + get_screenshots, + ios_api_analysis, + run_analysis, +) +from mobsf.DynamicAnalyzer.views.ios.device.dynamic_analyzer import ( + FAILED, + OK, + validate_and_connect_device, +) +from mobsf.DynamicAnalyzer.tools.webproxy import ( + get_http_tools_url, + stop_httptools, +) +from mobsf.MobSF.utils import ( + IOS_DEVICE_ID_REGEX, + SSH_DEVICE_ID_REGEX, + base64_decode, + get_md5, + is_md5, + key, + pretty_json, + print_n_send_error_response, + replace, + strict_package_check, +) +from mobsf.MobSF.views.authentication import ( + login_required, +) +from mobsf.DynamicAnalyzer.views.common.shared import ( + send_response, +) +from mobsf.MobSF.views.authorization import ( + Permissions, + permission_required, +) + + +logger = logging.getLogger(__name__) +register.filter('key', key) +register.filter('replace', replace) +register.filter('pretty_json', pretty_json) +register.filter('base64_decode', base64_decode) + + +def download_app_data_device(ios_device, checksum): + """Download App data from device.""" + app_dir = Path(settings.UPLD_DIR) / checksum + container_file = app_dir / 'mobsf_app_container_path.txt' + if container_file.exists(): + app_container = container_file.read_text( + 'utf-8').splitlines()[0].strip() + tarfile = f'/tmp/{checksum}-app-container.tar' + localtar = app_dir / f'{checksum}-app-container.tar' + ios_device.execute_command(f'tar -C {app_container} -cvf {tarfile} .') + ios_device.download_file(tarfile, localtar) + if localtar.exists(): + dst = Path(settings.DWD_DIR) / f'{checksum}-app_data.tar' + shutil.copyfile(localtar, dst) + +# JSON API + + +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def download_data_device(request, bundle_id, api=False): + """Download Application Data from Device.""" + logger.info('Downloading application data') + data = { + 'status': FAILED, + 'message': 'Failed to Download application data'} + try: + device_id = request.POST['device_id'] + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + if not strict_package_check(bundle_id): + data['message'] = 'Invalid iOS Bundle id' + return send_response(data, api) + checksum = get_md5(bundle_id.encode('utf-8')) + # App Container download + logger.info('Downloading app container data') + download_app_data_device(ios_device, checksum) + # Stop HTTPS Proxy + stop_httptools(get_http_tools_url(request)) + # Move HTTP raw logs to download directory + flows = Path.home() / '.httptools' / 'flows' + webf = flows / f'{bundle_id}.flow.txt' + dwd = Path(settings.DWD_DIR) + dweb = dwd / f'{checksum}-web_traffic.txt' + if webf.exists(): + shutil.copyfile(webf, dweb) + data = { + 'status': OK, + 'message': 'Downloaded application data', + } + except Exception as exp: + logger.exception('Downloading application data') + data['message'] = str(exp) + return send_response(data, api) + + +@login_required +def view_report_device(request, bundle_id, api=False): + """Dynamic Analysis Report Generation.""" + logger.info('iOS Dynamic Analysis Report Generation') + try: + if api: + device_id = request.POST.get('device_id') + else: + device_id = request.GET.get('device_id') + if device_id: + usb = IOS_DEVICE_ID_REGEX.match(device_id) + wifi = SSH_DEVICE_ID_REGEX.match(device_id) + if not (usb or wifi): + return print_n_send_error_response( + request, + 'Invalid device id or SSH connection string format', + api) + if not strict_package_check(bundle_id): + # bundle_id is not validated in REST API. + # Also bundleid is not strictly validated + # in URL path. + return print_n_send_error_response( + request, + 'Invalid iOS Bundle id', + api) + checksum = get_md5(bundle_id.encode('utf-8')) + app_dir = Path(settings.UPLD_DIR) / checksum + download_dir = settings.DWD_DIR + tools_dir = settings.TOOLS_DIR + frida_log = app_dir / 'mobsf_frida_out.txt' + data_dir = app_dir / 'DYNAMIC_DeviceData' + if not (frida_log.exists() or data_dir.exists()): + msg = ('Dynamic Analysis report is not available ' + 'for this app. Perform Dynamic Analysis ' + 'and generate the report.') + return print_n_send_error_response(request, msg, api) + api_analysis = ios_api_analysis(app_dir) + dump_analysis = run_analysis(app_dir, bundle_id, checksum) + trk = Trackers.Trackers(checksum, app_dir, tools_dir) + trackers = trk.get_trackers_domains_or_deps( + dump_analysis['domains'], None) + screenshots = get_screenshots(checksum, download_dir) + logger.info('Report generation completed') + context = { + 'hash': checksum, + 'device_id': device_id, + 'version': settings.MOBSF_VER, + 'title': 'iOS Dynamic Analysis Report', + 'bundleid': bundle_id, + 'trackers': trackers, + 'screenshots': screenshots, + 'frida_logs': frida_log.exists(), + } + context.update(api_analysis) + context.update(dump_analysis) + template = 'dynamic_analysis/ios/device/dynamic_report.html' + if api: + return context + return render(request, template, context) + except Exception as exp: + logger.exception('Dynamic Analysis Report Generation') + err = f'Error Generating Dynamic Analysis Report. {str(exp)}' + return print_n_send_error_response(request, err, api) + + +# JSON API + File Download +@login_required +@permission_required(Permissions.SCAN) +@require_http_methods(['POST']) +def download_file_device(request, api=False): + """Download file from device.""" + err_msg = 'Failed to download file' + data = { + 'status': FAILED, + 'message': err_msg, + } + try: + remote_path = request.POST['file'] + device_id = request.POST['device_id'] + ios_device, error = validate_and_connect_device(device_id) + if error: + data['message'] = error + return send_response(data, api) + file_obj = ios_device.download_file_object(remote_path) + if file_obj: + response = HttpResponse(file_obj, content_type='application/octet-stream') + response['Content-Disposition'] = ( + f'inline; filename={Path(remote_path).name}') + return response + else: + data['message'] = 'Failed to download file' + except Exception as exp: + logger.exception(err_msg) + data['message'] = str(exp) + return send_response(data, api) + + +# JSON API +@login_required +def ios_api_monitor(request, api=False): + try: + data = { + 'status': FAILED, + 'message': 'Failed to get API monitor data', + } + if api: + checksum = request.POST['checksum'] + stream = True + else: + checksum = request.GET.get('checksum', '') + stream = request.GET.get('stream', '') + bundle_id = request.GET.get('bundle_id', '') + if bundle_id and not strict_package_check(bundle_id): + data['message'] = 'Invalid iOS Bundle id' + return send_response(data, api) + if not is_md5(checksum): + data['message'] = 'Invalid checksum format' + return send_response(data, api) + if stream: + app_dir = Path(settings.UPLD_DIR) / checksum + apimon_file = app_dir / 'mobsf_dump_file.txt' + data = {} + if not apimon_file.exists(): + data['message'] = 'Data does not exist.' + return send_response(data, api) + # Read the file and parse each line as JSON + lines = apimon_file.read_text(encoding='utf-8').splitlines() + # remove all duplicate lines + lines = list(set(lines)) + api_data = [] + + for line in lines: + if line.strip(): + try: + # Parse each line as JSON + json_obj = json.loads(line) + + # Determine the API type based on the keys in the JSON object + api_type = None + if 'cookies' in json_obj: + api_type = 'Cookies' + elif 'keychain' in json_obj: + api_type = 'Keychain' + elif 'json' in json_obj: + api_type = 'JSON Data' + elif 'network' in json_obj: + api_type = 'Network Request' + elif 'sql' in json_obj: + api_type = 'SQL Query' + elif 'filename' in json_obj: + api_type = 'File Access' + elif 'crypto' in json_obj: + api_type = 'Crypto' + elif 'files' in json_obj: + api_type = 'Files' + elif 'nslog' in json_obj: + api_type = 'Logs' + elif 'credentialstorage' in json_obj: + api_type = 'Credentials' + elif 'nsuserdefaults' in json_obj: + api_type = 'UserDefaults' + elif 'pasteboard' in json_obj: + api_type = 'Pasteboard' + elif 'textinput' in json_obj: + api_type = 'Text Inputs' + elif 'datadir' in json_obj: + api_type = 'Data Directory' + else: + # If no known key, use the first key as API type + api_type = list(json_obj.keys())[ + 0] if json_obj else 'Unknown' + + # Create the data object for DataTable + api_data.append({ + 'api': api_type, + 'data': json_obj, + }) + except json.JSONDecodeError: + # Skip invalid JSON lines + continue + + data['data'] = api_data + return send_response(data, api) + template = 'dynamic_analysis/ios/api_monitor.html' + return render(request, + template, + {'checksum': checksum, + 'bundle_id': bundle_id, + 'version': settings.MOBSF_VER, + 'title': 'API Monitor'}) + except Exception: + logger.exception('API monitor streaming') + err = 'Error in API monitor streaming' + return print_n_send_error_response(request, err, api) diff --git a/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py b/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py index 2119ea8c..62db4929 100644 --- a/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py +++ b/mobsf/DynamicAnalyzer/views/ios/dynamic_analyzer.py @@ -3,7 +3,6 @@ import logging import os from pathlib import Path -from threading import Thread from django.conf import settings from django.shortcuts import render @@ -12,26 +11,20 @@ from mobsf.MobSF.utils import ( common_check, get_md5, print_n_send_error_response, - python_dict, strict_package_check, ) -from mobsf.StaticAnalyzer.models import StaticAnalyzerIOS from mobsf.DynamicAnalyzer.forms import UploadFileForm +from mobsf.DynamicAnalyzer.views.ios.helpers import ( + configure_proxy, + get_local_ipa_list, +) from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import ( generate_keypair_if_not_exists, ) -from mobsf.DynamicAnalyzer.tools.webproxy import ( - get_http_tools_url, - start_proxy, - stop_httptools, -) from mobsf.DynamicAnalyzer.views.ios.corellium_apis import ( CorelliumAPI, CorelliumInstanceAPI, ) -from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import ( - ssh_jumphost_reverse_port_forward, -) from mobsf.MobSF.views.authentication import ( login_required, ) @@ -48,30 +41,7 @@ logger = logging.getLogger(__name__) def dynamic_analysis(request, api=False): """The iOS Dynamic Analysis Entry point.""" try: - scan_apps = [] - ipas = StaticAnalyzerIOS.objects.filter( - FILE_NAME__endswith='.ipa') - for ipa in reversed(ipas): - bundle_hash = get_md5(ipa.BUNDLE_ID.encode('utf-8')) - frida_dump = Path( - settings.UPLD_DIR) / bundle_hash / 'mobsf_dump_file.txt' - macho = python_dict(ipa.MACHO_ANALYSIS) - encrypted = False - if (macho - and macho.get('encrypted') - and macho.get('encrypted').get('is_encrypted')): - encrypted = macho['encrypted']['is_encrypted'] - temp_dict = { - 'MD5': ipa.MD5, - 'APP_NAME': ipa.APP_NAME, - 'APP_VERSION': ipa.APP_VERSION, - 'FILE_NAME': ipa.FILE_NAME, - 'BUNDLE_ID': ipa.BUNDLE_ID, - 'BUNDLE_HASH': bundle_hash, - 'ENCRYPTED': encrypted, - 'DYNAMIC_REPORT_EXISTS': frida_dump.exists(), - } - scan_apps.append(temp_dict) + scan_apps = get_local_ipa_list() # Corellium instances = [] project_id = None @@ -103,17 +73,17 @@ def dynamic_analyzer(request, api=False): try: if api: bundleid = request.POST.get('bundle_id') + instance_id = request.POST.get('instance_id') + form = None else: - bundleid = request.GET.get('bundleid') + bundleid = request.GET.get('bundle_id') + instance_id = request.GET.get('instance_id') + form = UploadFileForm() if not bundleid or not strict_package_check(bundleid): return print_n_send_error_response( request, 'Invalid iOS Bundle id', api) - if api: - instance_id = request.POST.get('instance_id') - else: - instance_id = request.GET.get('instance_id') failed = common_check(instance_id) if failed: return print_n_send_error_response( @@ -126,12 +96,8 @@ def dynamic_analyzer(request, api=False): app_dir.mkdir() ci = CorelliumInstanceAPI(instance_id) configure_proxy(request, bundleid, ci) - if api: - form = None - else: - form = UploadFileForm() context = { - 'hash': bundle_hash, + 'checksum': bundle_hash, 'instance_id': instance_id, 'bundle_id': bundleid, 'version': settings.MOBSF_VER, @@ -185,16 +151,3 @@ def setup_ssh_keys(c): logger.error('Failed to add SSH Key to Corellium project') return logger.info('Added SSH Key to Corellium project') - - -def configure_proxy(request, project, ci): - """Configure HTTPS Proxy.""" - proxy_port = settings.PROXY_PORT - logger.info('Starting HTTPS Proxy on %s', proxy_port) - stop_httptools(get_http_tools_url(request)) - start_proxy(proxy_port, project) - # Remote Port forward for HTTPS Proxy - logger.info('Starting Remote Port Forward over SSH') - Thread(target=ssh_jumphost_reverse_port_forward, - args=(ci.get_ssh_connection_string(),), - daemon=True).start() diff --git a/mobsf/DynamicAnalyzer/views/ios/frida_core.py b/mobsf/DynamicAnalyzer/views/ios/frida_core.py index 092e5929..cfce79a3 100644 --- a/mobsf/DynamicAnalyzer/views/ios/frida_core.py +++ b/mobsf/DynamicAnalyzer/views/ios/frida_core.py @@ -1,13 +1,19 @@ import logging -from threading import Thread -from pathlib import Path import sys import time - -from django.conf import settings +from pathlib import Path +from threading import Thread import frida +from django.conf import settings + +from mobsf.DynamicAnalyzer.views.common.frida import ( + get_bridge_loader, +) +from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import ( + ssh_jumphost_port_forward, +) from mobsf.DynamicAnalyzer.views.ios.frida_auxiliary_scripts import ( class_pattern, class_trace, @@ -18,12 +24,9 @@ from mobsf.DynamicAnalyzer.views.ios.frida_auxiliary_scripts import ( string_capture, string_compare, ) -from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import ( - ssh_jumphost_port_forward, -) -from mobsf.DynamicAnalyzer.views.common.frida import ( - get_bridge_loader, -) + +SCREENSHOT_CAPTURED_MSG = '✅ Screenshot Captured' + logger = logging.getLogger(__name__) _PID = None @@ -61,6 +64,7 @@ class Frida: 'Are you able to run the app on ' 'this device?') self.bridge_loader = get_bridge_loader() + self.screenshot_event = None def get_scripts(self, script_type, selected_scripts): """Get Frida Scripts.""" @@ -157,6 +161,9 @@ class Frida: else: logger.debug('[Frida] %s', msg) self.write_log(self.frida_log, f'{msg}\n') + if (self.screenshot_event + and SCREENSHOT_CAPTURED_MSG in msg): + self.screenshot_event.set() else: logger.error('[Frida] %s', message) @@ -287,8 +294,15 @@ class Frida: self.container_file.write_text(self.app_container) except frida.InvalidOperationError: pass + # Migrating defaults to rpc functions + if 'ssl_pinning' in self.defaults: + api.bypass_ios_ssl_pinning() + if not self.extras: return + screenshot_action = self.extras.get('screenshot_action') + if screenshot_action == 'capture': + api.get_screenshot() raction = self.extras.get('rclass_action') rclass = self.extras.get('rclass_name') rclass_pattern = self.extras.get('rclass_pattern') diff --git a/mobsf/DynamicAnalyzer/views/ios/helpers.py b/mobsf/DynamicAnalyzer/views/ios/helpers.py new file mode 100644 index 00000000..999baa80 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/helpers.py @@ -0,0 +1,296 @@ +# -*- coding: utf_8 -*- +"""iOS helpers.""" +import logging +from threading import Thread +from pathlib import Path + +from django.conf import settings + +from mobsf.DynamicAnalyzer.tools.webproxy import ( + get_http_tools_url, + start_proxy, + stop_httptools, +) +from mobsf.MobSF.utils import ( + get_md5, + python_dict, +) +from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import ( + ssh_jumphost_reverse_port_forward, +) +from mobsf.StaticAnalyzer.models import StaticAnalyzerIOS + + +logger = logging.getLogger(__name__) + + +def get_local_ipa_list(): + """Get local ipa list.""" + scan_apps = [] + ipas = StaticAnalyzerIOS.objects.filter( + FILE_NAME__endswith='.ipa') + for ipa in reversed(ipas): + bundle_hash = get_md5(ipa.BUNDLE_ID.encode('utf-8')) + frida_dump = Path( + settings.UPLD_DIR) / bundle_hash / 'mobsf_dump_file.txt' + macho = python_dict(ipa.MACHO_ANALYSIS) + encrypted = False + if (macho + and macho.get('encrypted') + and macho.get('encrypted').get('is_encrypted')): + encrypted = macho['encrypted']['is_encrypted'] + temp_dict = { + 'MD5': ipa.MD5, + 'APP_NAME': ipa.APP_NAME, + 'APP_VERSION': ipa.APP_VERSION, + 'FILE_NAME': ipa.FILE_NAME, + 'BUNDLE_ID': ipa.BUNDLE_ID, + 'BUNDLE_HASH': bundle_hash, + 'ENCRYPTED': encrypted, + 'DYNAMIC_REPORT_EXISTS': frida_dump.exists(), + } + scan_apps.append(temp_dict) + return scan_apps + + +def get_ios_model_mapping(): + """Get iOS model mapping.""" + return { + # iPhone Simulator + 'i386': 'iPhone Simulator', + 'x86_64': 'iPhone Simulator', + 'arm64': 'iPhone Simulator', + + # iPhone Models + 'iPhone1,1': 'iPhone', + 'iPhone1,2': 'iPhone 3G', + 'iPhone2,1': 'iPhone 3GS', + 'iPhone3,1': 'iPhone 4', + 'iPhone3,2': 'iPhone 4 GSM Rev A', + 'iPhone3,3': 'iPhone 4 CDMA', + 'iPhone4,1': 'iPhone 4S', + 'iPhone5,1': 'iPhone 5 (GSM)', + 'iPhone5,2': 'iPhone 5 (GSM+CDMA)', + 'iPhone5,3': 'iPhone 5C (GSM)', + 'iPhone5,4': 'iPhone 5C (Global)', + 'iPhone6,1': 'iPhone 5S (GSM)', + 'iPhone6,2': 'iPhone 5S (Global)', + 'iPhone7,1': 'iPhone 6 Plus', + 'iPhone7,2': 'iPhone 6', + 'iPhone8,1': 'iPhone 6s', + 'iPhone8,2': 'iPhone 6s Plus', + 'iPhone8,4': 'iPhone SE (GSM)', + 'iPhone9,1': 'iPhone 7', + 'iPhone9,2': 'iPhone 7 Plus', + 'iPhone9,3': 'iPhone 7', + 'iPhone9,4': 'iPhone 7 Plus', + 'iPhone10,1': 'iPhone 8', + 'iPhone10,2': 'iPhone 8 Plus', + 'iPhone10,3': 'iPhone X Global', + 'iPhone10,4': 'iPhone 8', + 'iPhone10,5': 'iPhone 8 Plus', + 'iPhone10,6': 'iPhone X GSM', + 'iPhone11,2': 'iPhone XS', + 'iPhone11,4': 'iPhone XS Max', + 'iPhone11,6': 'iPhone XS Max Global', + 'iPhone11,8': 'iPhone XR', + 'iPhone12,1': 'iPhone 11', + 'iPhone12,3': 'iPhone 11 Pro', + 'iPhone12,5': 'iPhone 11 Pro Max', + 'iPhone12,8': 'iPhone SE 2nd Gen', + 'iPhone13,1': 'iPhone 12 Mini', + 'iPhone13,2': 'iPhone 12', + 'iPhone13,3': 'iPhone 12 Pro', + 'iPhone13,4': 'iPhone 12 Pro Max', + 'iPhone14,2': 'iPhone 13 Pro', + 'iPhone14,3': 'iPhone 13 Pro Max', + 'iPhone14,4': 'iPhone 13 Mini', + 'iPhone14,5': 'iPhone 13', + 'iPhone14,6': 'iPhone SE 3rd Gen', + 'iPhone14,7': 'iPhone 14', + 'iPhone14,8': 'iPhone 14 Plus', + 'iPhone15,2': 'iPhone 14 Pro', + 'iPhone15,3': 'iPhone 14 Pro Max', + 'iPhone15,4': 'iPhone 15', + 'iPhone15,5': 'iPhone 15 Plus', + 'iPhone16,1': 'iPhone 15 Pro', + 'iPhone16,2': 'iPhone 15 Pro Max', + 'iPhone17,1': 'iPhone 16 Pro', + 'iPhone17,2': 'iPhone 16 Pro Max', + 'iPhone17,3': 'iPhone 16', + 'iPhone17,4': 'iPhone 16 Plus', + 'iPhone17,5': 'iPhone 16e', + + # iPod Models + 'iPod1,1': '1st Gen iPod', + 'iPod2,1': '2nd Gen iPod', + 'iPod3,1': '3rd Gen iPod', + 'iPod4,1': '4th Gen iPod', + 'iPod5,1': '5th Gen iPod', + 'iPod7,1': '6th Gen iPod', + 'iPod9,1': '7th Gen iPod', + + # iPad Models + 'iPad1,1': 'iPad', + 'iPad1,2': 'iPad 3G', + 'iPad2,1': '2nd Gen iPad', + 'iPad2,2': '2nd Gen iPad GSM', + 'iPad2,3': '2nd Gen iPad CDMA', + 'iPad2,4': '2nd Gen iPad New Revision', + 'iPad3,1': '3rd Gen iPad', + 'iPad3,2': '3rd Gen iPad CDMA', + 'iPad3,3': '3rd Gen iPad GSM', + 'iPad2,5': 'iPad mini', + 'iPad2,6': 'iPad mini GSM+LTE', + 'iPad2,7': 'iPad mini CDMA+LTE', + 'iPad3,4': '4th Gen iPad', + 'iPad3,5': '4th Gen iPad GSM+LTE', + 'iPad3,6': '4th Gen iPad CDMA+LTE', + 'iPad4,1': 'iPad Air (WiFi)', + 'iPad4,2': 'iPad Air (GSM+CDMA)', + 'iPad4,3': '1st Gen iPad Air (China)', + 'iPad4,4': 'iPad mini Retina (WiFi)', + 'iPad4,5': 'iPad mini Retina (GSM+CDMA)', + 'iPad4,6': 'iPad mini Retina (China)', + 'iPad4,7': 'iPad mini 3 (WiFi)', + 'iPad4,8': 'iPad mini 3 (GSM+CDMA)', + 'iPad4,9': 'iPad Mini 3 (China)', + 'iPad5,1': 'iPad mini 4 (WiFi)', + 'iPad5,2': '4th Gen iPad mini (WiFi+Cellular)', + 'iPad5,3': 'iPad Air 2 (WiFi)', + 'iPad5,4': 'iPad Air 2 (Cellular)', + 'iPad6,3': 'iPad Pro (9.7 inch, WiFi)', + 'iPad6,4': 'iPad Pro (9.7 inch, WiFi+LTE)', + 'iPad6,7': 'iPad Pro (12.9 inch, WiFi)', + 'iPad6,8': 'iPad Pro (12.9 inch, WiFi+LTE)', + 'iPad6,11': 'iPad (2017)', + 'iPad6,12': 'iPad (2017)', + 'iPad7,1': 'iPad Pro 2nd Gen (WiFi)', + 'iPad7,2': 'iPad Pro 2nd Gen (WiFi+Cellular)', + 'iPad7,3': 'iPad Pro 10.5-inch 2nd Gen', + 'iPad7,4': 'iPad Pro 10.5-inch 2nd Gen', + 'iPad7,5': 'iPad 6th Gen (WiFi)', + 'iPad7,6': 'iPad 6th Gen (WiFi+Cellular)', + 'iPad7,11': 'iPad 7th Gen 10.2-inch (WiFi)', + 'iPad7,12': 'iPad 7th Gen 10.2-inch (WiFi+Cellular)', + 'iPad8,1': 'iPad Pro 11 inch 3rd Gen (WiFi)', + 'iPad8,2': 'iPad Pro 11 inch 3rd Gen (1TB, WiFi)', + 'iPad8,3': 'iPad Pro 11 inch 3rd Gen (WiFi+Cellular)', + 'iPad8,4': 'iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular)', + 'iPad8,5': 'iPad Pro 12.9 inch 3rd Gen (WiFi)', + 'iPad8,6': 'iPad Pro 12.9 inch 3rd Gen (1TB, WiFi)', + 'iPad8,7': 'iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular)', + 'iPad8,8': 'iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular)', + 'iPad8,9': 'iPad Pro 11 inch 4th Gen (WiFi)', + 'iPad8,10': 'iPad Pro 11 inch 4th Gen (WiFi+Cellular)', + 'iPad8,11': 'iPad Pro 12.9 inch 4th Gen (WiFi)', + 'iPad8,12': 'iPad Pro 12.9 inch 4th Gen (WiFi+Cellular)', + 'iPad11,1': 'iPad mini 5th Gen (WiFi)', + 'iPad11,2': 'iPad mini 5th Gen', + 'iPad11,3': 'iPad Air 3rd Gen (WiFi)', + 'iPad11,4': 'iPad Air 3rd Gen', + 'iPad11,6': 'iPad 8th Gen (WiFi)', + 'iPad11,7': 'iPad 8th Gen (WiFi+Cellular)', + 'iPad12,1': 'iPad 9th Gen (WiFi)', + 'iPad12,2': 'iPad 9th Gen (WiFi+Cellular)', + 'iPad14,1': 'iPad mini 6th Gen (WiFi)', + 'iPad14,2': 'iPad mini 6th Gen (WiFi+Cellular)', + 'iPad13,1': 'iPad Air 4th Gen (WiFi)', + 'iPad13,2': 'iPad Air 4th Gen (WiFi+Cellular)', + 'iPad13,4': 'iPad Pro 11 inch 5th Gen', + 'iPad13,5': 'iPad Pro 11 inch 5th Gen', + 'iPad13,6': 'iPad Pro 11 inch 5th Gen', + 'iPad13,7': 'iPad Pro 11 inch 5th Gen', + 'iPad13,8': 'iPad Pro 12.9 inch 5th Gen', + 'iPad13,9': 'iPad Pro 12.9 inch 5th Gen', + 'iPad13,10': 'iPad Pro 12.9 inch 5th Gen', + 'iPad13,11': 'iPad Pro 12.9 inch 5th Gen', + 'iPad13,16': 'iPad Air 5th Gen (WiFi)', + 'iPad13,17': 'iPad Air 5th Gen (WiFi+Cellular)', + 'iPad13,18': 'iPad 10th Gen', + 'iPad13,19': 'iPad 10th Gen', + 'iPad14,3': 'iPad Pro 11 inch 4th Gen', + 'iPad14,4': 'iPad Pro 11 inch 4th Gen', + 'iPad14,5': 'iPad Pro 12.9 inch 6th Gen', + 'iPad14,6': 'iPad Pro 12.9 inch 6th Gen', + 'iPad14,8': 'iPad Air 11 inch 6th Gen (WiFi)', + 'iPad14,9': 'iPad Air 11 inch 6th Gen (WiFi+Cellular)', + 'iPad14,10': 'iPad Air 13 inch 6th Gen (WiFi)', + 'iPad14,11': 'iPad Air 13 inch 6th Gen (WiFi+Cellular)', + 'iPad15,3': 'iPad Air 11-inch 7th Gen (WiFi)', + 'iPad15,4': 'iPad Air 11-inch 7th Gen (WiFi+Cellular)', + 'iPad15,5': 'iPad Air 13-inch 7th Gen (WiFi)', + 'iPad15,6': 'iPad Air 13-inch 7th Gen (WiFi+Cellular)', + 'iPad15,7': 'iPad 11th Gen (WiFi)', + 'iPad15,8': 'iPad 11th Gen (WiFi+Cellular)', + 'iPad16,1': 'iPad mini 7th Gen (WiFi)', + 'iPad16,2': 'iPad mini 7th Gen (WiFi+Cellular)', + 'iPad16,3': 'iPad Pro 11 inch 5th Gen', + 'iPad16,4': 'iPad Pro 11 inch 5th Gen', + 'iPad16,5': 'iPad Pro 12.9 inch 7th Gen', + 'iPad16,6': 'iPad Pro 12.9 inch 7th Gen', + + # Apple Watch Models + 'Watch1,1': 'Apple Watch 38mm case', + 'Watch1,2': 'Apple Watch 42mm case', + 'Watch2,6': 'Apple Watch Series 1 38mm case', + 'Watch2,7': 'Apple Watch Series 1 42mm case', + 'Watch2,3': 'Apple Watch Series 2 38mm case', + 'Watch2,4': 'Apple Watch Series 2 42mm case', + 'Watch3,1': 'Apple Watch Series 3 38mm case (GPS+Cellular)', + 'Watch3,2': 'Apple Watch Series 3 42mm case (GPS+Cellular)', + 'Watch3,3': 'Apple Watch Series 3 38mm case (GPS)', + 'Watch3,4': 'Apple Watch Series 3 42mm case (GPS)', + 'Watch4,1': 'Apple Watch Series 4 40mm case (GPS)', + 'Watch4,2': 'Apple Watch Series 4 44mm case (GPS)', + 'Watch4,3': 'Apple Watch Series 4 40mm case (GPS+Cellular)', + 'Watch4,4': 'Apple Watch Series 4 44mm case (GPS+Cellular)', + 'Watch5,1': 'Apple Watch Series 5 40mm case (GPS)', + 'Watch5,2': 'Apple Watch Series 5 44mm case (GPS)', + 'Watch5,3': 'Apple Watch Series 5 40mm case (GPS+Cellular)', + 'Watch5,4': 'Apple Watch Series 5 44mm case (GPS+Cellular)', + 'Watch5,9': 'Apple Watch SE 40mm case (GPS)', + 'Watch5,10': 'Apple Watch SE 44mm case (GPS)', + 'Watch5,11': 'Apple Watch SE 40mm case (GPS+Cellular)', + 'Watch5,12': 'Apple Watch SE 44mm case (GPS+Cellular)', + 'Watch6,1': 'Apple Watch Series 6 40mm case (GPS)', + 'Watch6,2': 'Apple Watch Series 6 44mm case (GPS)', + 'Watch6,3': 'Apple Watch Series 6 40mm case (GPS+Cellular)', + 'Watch6,4': 'Apple Watch Series 6 44mm case (GPS+Cellular)', + 'Watch6,6': 'Apple Watch Series 7 41mm case (GPS)', + 'Watch6,7': 'Apple Watch Series 7 45mm case (GPS)', + 'Watch6,8': 'Apple Watch Series 7 41mm case (GPS+Cellular)', + 'Watch6,9': 'Apple Watch Series 7 45mm case (GPS+Cellular)', + 'Watch6,10': 'Apple Watch SE 40mm case (GPS)', + 'Watch6,11': 'Apple Watch SE 44mm case (GPS)', + 'Watch6,12': 'Apple Watch SE 40mm case (GPS+Cellular)', + 'Watch6,13': 'Apple Watch SE 44mm case (GPS+Cellular)', + 'Watch6,14': 'Apple Watch Series 8 41mm case (GPS)', + 'Watch6,15': 'Apple Watch Series 8 45mm case (GPS)', + 'Watch6,16': 'Apple Watch Series 8 41mm case (GPS+Cellular)', + 'Watch6,17': 'Apple Watch Series 8 45mm case (GPS+Cellular)', + 'Watch6,18': 'Apple Watch Ultra', + 'Watch7,1': 'Apple Watch Series 9 41mm case (GPS)', + 'Watch7,2': 'Apple Watch Series 9 45mm case (GPS)', + 'Watch7,3': 'Apple Watch Series 9 41mm case (GPS+Cellular)', + 'Watch7,4': 'Apple Watch Series 9 45mm case (GPS+Cellular)', + 'Watch7,5': 'Apple Watch Ultra 2', + 'Watch7,8': 'Apple Watch Series 10 42mm case (GPS)', + 'Watch7,9': 'Apple Watch Series 10 46mm case (GPS)', + 'Watch7,10': 'Apple Watch Series 10 42mm case (GPS+Cellular)', + 'Watch7,11': 'Apple Watch Series 10 46mm (GPS+Cellular)', + } + + +def configure_proxy(request, project, ci=None): + """Configure HTTPS Proxy.""" + proxy_port = settings.PROXY_PORT + logger.info('Starting HTTPS Proxy on %s', proxy_port) + stop_httptools(get_http_tools_url(request)) + start_proxy(proxy_port, project) + if ci: + # Remote Port forward for HTTPS Proxy + logger.info('Starting Remote Port Forward over SSH') + Thread(target=ssh_jumphost_reverse_port_forward, + args=(ci.get_ssh_connection_string(),), + daemon=True).start() diff --git a/mobsf/DynamicAnalyzer/views/ios/report.py b/mobsf/DynamicAnalyzer/views/ios/report.py index a57846be..3c090c83 100644 --- a/mobsf/DynamicAnalyzer/views/ios/report.py +++ b/mobsf/DynamicAnalyzer/views/ios/report.py @@ -61,16 +61,17 @@ def ios_view_report(request, bundle_id, api=False): download_dir = settings.DWD_DIR tools_dir = settings.TOOLS_DIR frida_log = app_dir / 'mobsf_frida_out.txt' - if not frida_log.exists(): + data_dir = app_dir / 'DYNAMIC_DeviceData' + if not (frida_log.exists() or data_dir.exists()): msg = ('Dynamic Analysis report is not available ' 'for this app. Perform Dynamic Analysis ' 'and generate the report.') return print_n_send_error_response(request, msg, api) api_analysis = ios_api_analysis(app_dir) - dump_analaysis = run_analysis(app_dir, bundle_id, checksum) + dump_analysis = run_analysis(app_dir, bundle_id, checksum) trk = Trackers.Trackers(checksum, app_dir, tools_dir) trackers = trk.get_trackers_domains_or_deps( - dump_analaysis['domains'], None) + dump_analysis['domains'], None) screenshots = get_screenshots(checksum, download_dir) context = { 'hash': checksum, @@ -83,7 +84,7 @@ def ios_view_report(request, bundle_id, api=False): 'frida_logs': frida_log.exists(), } context.update(api_analysis) - context.update(dump_analaysis) + context.update(dump_analysis) template = 'dynamic_analysis/ios/dynamic_report.html' if api: return context diff --git a/mobsf/MobSF/init.py b/mobsf/MobSF/init.py index d75ac961..20fe687b 100644 --- a/mobsf/MobSF/init.py +++ b/mobsf/MobSF/init.py @@ -18,13 +18,13 @@ from mobsf.install.windows.setup import windows_config_local logger = logging.getLogger(__name__) -VERSION = '4.4.6' +VERSION = '4.5.0' BANNER = r""" - __ __ _ ____ _____ _ _ _ _ - | \/ | ___ | |__/ ___|| ___|_ _| || | | || | - | |\/| |/ _ \| '_ \___ \| |_ \ \ / / || |_| || |_ - | | | | (_) | |_) |__) | _| \ V /|__ _|__ _| - |_| |_|\___/|_.__/____/|_| \_/ |_|(_) |_| + __ __ _ ____ _____ _ _ ____ + | \/ | ___ | |__/ ___|| ___|_ _| || | | ___| + | |\/| |/ _ \| '_ \___ \| |_ \ \ / / || |_ |___ \ + | | | | (_) | |_) |__) | _| \ V /|__ _| ___) | + |_| |_|\___/|_.__/____/|_| \_/ |_|(_)____/ """ # noqa: W291 # ASCII Font: Standard diff --git a/mobsf/MobSF/security.py b/mobsf/MobSF/security.py index 24b730e0..f96ae706 100644 --- a/mobsf/MobSF/security.py +++ b/mobsf/MobSF/security.py @@ -107,6 +107,11 @@ def get_executable_hashes(): 'nuget.exe', 'where.exe', 'wkhtmltopdf.exe', + 'idevice_id', + 'ideviceinfo', + 'idevicename', + 'pkill', + 'iproxy', ] for sbin in system_bins: bin_path = which(sbin) diff --git a/mobsf/MobSF/settings.py b/mobsf/MobSF/settings.py index 5e338f44..ff1f5892 100644 --- a/mobsf/MobSF/settings.py +++ b/mobsf/MobSF/settings.py @@ -532,6 +532,16 @@ else: # if VT_UPLOAD is set to True. # =============================================== # =======IOS DYNAMIC ANALYSIS SETTINGS=========== + # Should be SSH IP:PORT, example: 192.168.1.100:22 + # Field also supports multiple devices, example: 192.168.1.100:22,192.168.1.101:22 + IOS_ANALYZER_IDENTIFIERS = os.getenv('MOBSF_IOS_ANALYZER_IDENTIFIERS', '') + # SSH credentials for jailbroken iOS device (USB or WiFi path) + # Defaults match the standard jailbreak SSH default (root/alpine) + IOS_SSH_USER = os.getenv('MOBSF_IOS_SSH_USER', 'root') + IOS_SSH_PASSWORD = os.getenv('MOBSF_IOS_SSH_PASSWORD', 'alpine') + # ============================================== + + # =======IOS DYNAMIC ANALYSIS CORELLIUM SETTINGS=========== CORELLIUM_API_DOMAIN = os.getenv('MOBSF_CORELLIUM_API_DOMAIN', '') CORELLIUM_API_KEY = os.getenv('MOBSF_CORELLIUM_API_KEY', '') CORELLIUM_PROJECT_ID = os.getenv('MOBSF_CORELLIUM_PROJECT_ID', '') diff --git a/mobsf/MobSF/urls.py b/mobsf/MobSF/urls.py index 5e7330d4..da90c1cc 100755 --- a/mobsf/MobSF/urls.py +++ b/mobsf/MobSF/urls.py @@ -15,6 +15,12 @@ from mobsf.DynamicAnalyzer.views.ios import ( report as ios_view_report, tests_frida as ios_tests_frida, ) +from mobsf.DynamicAnalyzer.views.ios.device import ( + dynamic_analyzer as ios_device, +) +from mobsf.DynamicAnalyzer.views.ios.device import ( + report as device_report, +) from mobsf.MobSF import utils from mobsf.MobSF.security import ( init_exec_hooks, @@ -29,6 +35,7 @@ from mobsf.MobSF.views import ( from mobsf.MobSF.views.api import api_static_analysis as api_sz from mobsf.MobSF.views.api import api_android_dynamic_analysis as api_dz from mobsf.MobSF.views.api import api_ios_dynamic_analysis as api_idz +from mobsf.MobSF.views.api import api_ios_device_analysis as api_iddz from mobsf.StaticAnalyzer import tests from mobsf.StaticAnalyzer.views.common import ( appsec, @@ -123,7 +130,7 @@ urlpatterns = [ re_path(r'^api/v1/frida/list_scripts$', api_dz.api_list_frida_scripts), re_path(r'^api/v1/frida/get_script$', api_dz.api_get_script_content), re_path(r'^api/v1/dynamic/view_source$', api_dz.api_dynamic_view_file), - # iOS Specific + # iOS Corellium re_path(r'^api/v1/ios/corellium_supported_models$', api_idz.api_corellium_get_supported_models), re_path(r'^api/v1/ios/corellium_ios_versions$', @@ -177,6 +184,33 @@ urlpatterns = [ # Frida re_path(r'^api/v1/frida/ios_instrument$', api_idz.api_ios_instrument), re_path(r'^api/v1/dynamic/ios_report_json$', api_idz.api_ios_view_report), + # iOS Jailbroken Device + re_path(r'^api/v1/ios/device_dynamic_analysis$', + api_iddz.api_device_dynamic_analysis), + re_path(r'^api/v1/ios/device_get$', + api_iddz.api_device_get), + re_path(r'^api/v1/ios/device_install_ipa$', + api_iddz.api_device_install_ipa), + re_path(r'^api/v1/ios/device_dynamic_analyzer$', + api_iddz.api_device_dynamic_analyzer), + re_path(r'^api/v1/ios/device_file_upload$', + api_iddz.api_device_file_upload), + re_path(r'^api/v1/ios/device_ssh_execute$', + api_iddz.api_device_ssh_execute), + re_path(r'^api/v1/ios/device_instrument$', + api_iddz.api_device_instrument), + re_path(r'^api/v1/ios/device_system_logs$', + api_iddz.api_device_system_logs), + re_path(r'^api/v1/ios/device_ps$', + api_iddz.api_device_ps), + re_path(r'^api/v1/ios/device_file_download$', + api_iddz.api_device_file_download), + re_path(r'^api/v1/ios/device_api_monitor$', + api_iddz.api_device_api_monitor), + re_path(r'^api/v1/ios/device_download_app_data$', + api_iddz.api_device_download_app_data), + re_path(r'^api/v1/ios/device_report_json$', + api_iddz.api_device_report_json), ] if settings.API_ONLY == '0': urlpatterns.extend([ @@ -399,7 +433,46 @@ if settings.API_ONLY == '0': re_path(fr'^ios/view_report/{bundle_id_regex}', ios_view_report.ios_view_report, name='ios_view_report'), - + # iOS Device + re_path(r'^ios/dynamic_analysis_device/$', + ios_device.dynamic_analysis_device, + name='dynamic_ios_device'), + re_path(r'^ios/ios_device/$', + ios_device.get_ios_device, + name='get_ios_device'), + re_path(r'^ios/install_ipa_device/$', + ios_device.install_ipa_device, + name='install_ipa_device'), + re_path(r'^ios/dynamic_analyzer_device/$', + ios_device.dynamic_analyzer_device, + name='dynamic_analyzer_device'), + re_path(r'^ios/upload_file_device/$', + ios_device.upload_file_device, + name='upload_file_device'), + re_path(r'^ios/ssh_execute_device/$', + ios_device.ssh_execute_device, + name='ssh_execute_device'), + re_path(r'^ios/instrument_device/$', + ios_device.ios_instrument_device, + name='ios_instrument_device'), + re_path(r'^ios/system_logs_device/$', + ios_device.system_logs_device, + name='system_logs_device'), + re_path(r'^ios/ps_device/$', + ios_device.ps_device, + name='ps_device'), + re_path(r'^ios/download_file_device/$', + device_report.download_file_device, + name='download_file_device'), + re_path(r'^ios/api_monitor/$', + device_report.ios_api_monitor, + name='ios_api_monitor'), + re_path(fr'^ios/download_data_device/{bundle_id_regex}', + device_report.download_data_device, + name='download_data_device'), + re_path(fr'^ios/view_report_device/{bundle_id_regex}', + device_report.view_report_device, + name='view_report_device'), # Test re_path(r'^tests/$', tests.start_test), ]) diff --git a/mobsf/MobSF/utils.py b/mobsf/MobSF/utils.py index d0156869..f49ba898 100755 --- a/mobsf/MobSF/utils.py +++ b/mobsf/MobSF/utils.py @@ -63,6 +63,9 @@ GOOGLE_API_KEY_REGEX = re.compile(r'AIza[0-9A-Za-z-_]{35}$') GOOGLE_APP_ID_REGEX = re.compile(r'\d{1,2}:\d{1,50}:android:[a-f0-9]{1,50}') PKG_REGEX = re.compile( r'package\s+([a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*);') +IOS_DEVICE_ID_REGEX = re.compile(r'^[a-fA-F0-9-]{20,40}$') +SSH_DEVICE_ID_REGEX = re.compile( + r'^(?:\[(?P[^\]]+)\]|(?P[^:\[\]]+)):(?P\d{1,5})$') class Color(object): @@ -1001,3 +1004,16 @@ def set_permissions(path): item.chmod(perm_file) except Exception: pass + + +def parse_host_port(text): + # Match [IPv6]:port OR host:port + ipv6_match = re.match(r'^\[(.+)\]:(\d+)$', text) + if ipv6_match: + return ipv6_match.group(1), int(ipv6_match.group(2)) + + ipv4_match = re.match(r'^([^:]+):(\d+)$', text) + if ipv4_match: + return ipv4_match.group(1), int(ipv4_match.group(2)) + + raise ValueError('Input must be in format host:port or [IPv6]:port') diff --git a/mobsf/MobSF/views/api/api_ios_device_analysis.py b/mobsf/MobSF/views/api/api_ios_device_analysis.py new file mode 100644 index 00000000..e63dc2ea --- /dev/null +++ b/mobsf/MobSF/views/api/api_ios_device_analysis.py @@ -0,0 +1,211 @@ +# -*- coding: utf_8 -*- +"""MobSF REST API V 1 - iOS Jailbroken Device.""" + +from django.views.decorators.csrf import csrf_exempt + +from mobsf.MobSF.views.helpers import request_method +from mobsf.MobSF.views.api.api_middleware import make_api_response +from mobsf.DynamicAnalyzer.views.ios.device import ( + dynamic_analyzer as ios_device, + report as device_report, +) + + +FAILED = 'failed' +ERROR = 'error' +DEVICE_ID = 'device_id' +BUNDLE_ID = 'bundle_id' + + +@request_method(['POST']) +@csrf_exempt +def api_device_dynamic_analysis(request): + """POST - iOS Device Dynamic Analysis Entrypoint.""" + if DEVICE_ID not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.dynamic_analysis_device(request, True) + if ERROR in resp: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_get(request): + """POST - Get iOS Device Details and List Apps.""" + if DEVICE_ID not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.get_ios_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_install_ipa(request): + """POST - Install IPA on iOS Device.""" + params = {DEVICE_ID, 'checksum'} + if set(request.POST) < params: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.install_ipa_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_dynamic_analyzer(request): + """POST - iOS Device Dynamic Analyzer.""" + params = {DEVICE_ID, BUNDLE_ID} + if set(request.POST) < params: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.dynamic_analyzer_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_file_upload(request): + """POST - Upload file to iOS Device.""" + if DEVICE_ID not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + if 'file' not in request.FILES: + return make_api_response( + {'error': 'Missing File'}, 422) + resp = ios_device.upload_file_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_ssh_execute(request): + """POST - Execute commands on iOS Device over SSH.""" + params = {DEVICE_ID, 'cmd'} + if set(request.POST) < params: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.ssh_execute_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_instrument(request): + """POST - Frida Instrument on iOS Device.""" + params = { + DEVICE_ID, + BUNDLE_ID, + 'default_hooks', + 'dump_hooks', + 'auxiliary_hooks', + 'frida_code'} + if set(request.POST) < params: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.ios_instrument_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_system_logs(request): + """POST - Get system logs from iOS Device.""" + if DEVICE_ID not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.system_logs_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_ps(request): + """POST - Get running processes on iOS Device.""" + if DEVICE_ID not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = ios_device.ps_device(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_file_download(request): + """POST - Download file from iOS Device.""" + params = {DEVICE_ID, 'file'} + if set(request.POST) < params: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = device_report.download_file_device(request, True) + if resp.get('Content-Disposition'): + # file http response + return resp + elif resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_api_monitor(request): + """POST - iOS Device API Monitor.""" + if 'checksum' not in request.POST: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = device_report.ios_api_monitor(request, True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_download_app_data(request): + """POST - Download Application Data from iOS Device.""" + params = {DEVICE_ID, BUNDLE_ID} + if set(request.POST) < params: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = device_report.download_data_device( + request, + request.POST[BUNDLE_ID], + True) + if resp.get('status') == FAILED: + return make_api_response(resp, 500) + return make_api_response(resp, 200) + + +@request_method(['POST']) +@csrf_exempt +def api_device_report_json(request): + """POST - iOS Device Dynamic Analysis Report.""" + params = {DEVICE_ID, BUNDLE_ID} + if set(request.POST) < params: + return make_api_response( + {'error': 'Missing Parameters'}, 422) + resp = device_report.view_report_device( + request, + request.POST[BUNDLE_ID], + True) + if 'error' in resp: + return make_api_response(resp, 500) + return make_api_response(resp, 200) diff --git a/mobsf/static/others/css/spinner.css b/mobsf/static/others/css/spinner.css index bc0357e7..9c11a757 100644 --- a/mobsf/static/others/css/spinner.css +++ b/mobsf/static/others/css/spinner.css @@ -10,7 +10,9 @@ left: 0; width: 100%; height: 100%; - background-color: rgba(16, 16, 16, 0.5); + background-color: rgba(16, 16, 16, 0.7); + z-index: 9999; + pointer-events: auto; } @-webkit-keyframes uil-ring-anim { diff --git a/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html b/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html index 9c0e66da..7c6e0c91 100644 --- a/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html +++ b/mobsf/templates/dynamic_analysis/android/dynamic_analyzer.html @@ -12,7 +12,7 @@ +{% endblock %} +{% block content %} +

+
+
+
+ + +
+
+
+ +
+
+
+

MobSF iOS Dynamic Analyzer

+ +
+
+

+

Choose a device for Dynamic Analysis:
+ +

+
+
+ +
+
+
+ +
+ +
+ + +
+
+
+

MobSF Dynamic Analysis

+
+
+
+

Apps in Device

+ + + +
+
+ +

Apps Available

+ + + + + + + + + + + {% if apps %} + {% for e in apps %} + + + + + + + {% endfor %} + {% endif %} + +
APPFILE NAMEBUNDLE IDACTION
+ +
{{ e.APP_NAME }} - {{ e.APP_VERSION }} +
+ {{ e.FILE_NAME }} + + {{ e.BUNDLE_ID }} + + {% if e.ENCRYPTED %} + ENCRYPTED IPA
+ You need decrypted IPA for dynamic analysis. + {% else %} +

+ Upload & Install +

+

+ View Report +

+ {% endif %} +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + + + + + + +{% endblock %} +{% block extra_scripts %} + + + + + +{% endblock %} diff --git a/mobsf/templates/dynamic_analysis/ios/device/dynamic_analyzer.html b/mobsf/templates/dynamic_analysis/ios/device/dynamic_analyzer.html new file mode 100644 index 00000000..ac3c05a3 --- /dev/null +++ b/mobsf/templates/dynamic_analysis/ios/device/dynamic_analyzer.html @@ -0,0 +1,1063 @@ + +{% extends "base/base_layout.html" %} +{% load static %} +{% block sidebar_option %} + sidebar-collapse +{% endblock %} +{% block extra_css %} + + + + + + + + +{% endblock %} +{% block content %} + + + +
+
+
+
+
+

Dynamic Analyzer - {{ bundle_id }}

+
+
+
+
+
+ + +
+ + + +
+
+ +
+ +
+
+ +
+
+
+
+ + +
+ + +
+
+
+
+

Default Frida Scripts

+ + +
+
+
+

Trace Frida Scripts

+ + + + + + + + + + + + + +
+
+
+

Auxiliary Frida Scripts

+ + + + +
+ + + + + + + + +
+ +
+ +
+
+

Instrumentation

+
+ +
+
+ + + +
+
+ +
+
+
+ +
+ + +
+ +
+ +
+ + +
+
+
+

Frida Code Editor

+ +
+ +
+
+ +
+
+
+ Available Scripts (Use CTRL to choose multiple) + +
+
+ +
+ +
+ +
+ +
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+
+ +
+ +
+ +
+ + + + +
+
+ +
+
+ +

Data refreshed in every 3 seconds.
+

+                  
+
+ +
+ +
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} +{% block extra_scripts %} + + + + + + + + +{% endblock %} diff --git a/mobsf/templates/dynamic_analysis/ios/device/dynamic_report.html b/mobsf/templates/dynamic_analysis/ios/device/dynamic_report.html new file mode 100644 index 00000000..900ccdea --- /dev/null +++ b/mobsf/templates/dynamic_analysis/ios/device/dynamic_report.html @@ -0,0 +1,1485 @@ +{% extends "base/base_layout.html" %} + {% load static %} + {% block sidebar_option %} + sidebar-mini + {% endblock %} + {% block extra_css %} + + + + + + + {% endblock %} + + {% block sidebar %} + + + + +{% endblock %} +{% block content %} + +
+ +
+ +
+
+
+
+

Dynamic Analysis Report - {{ bundleid }}

+
+
+
+
+
+ + + + + +
+
+
+
+
+
+
+
+
+

INFORMATION

+
+

+ {% if frida_logs %} + API Monitor Logs + Frida Logs View + {% endif %} + Start HTTPTools +

+

Raw Logs

+

+ HTTP(S) Traffic + Application Data +

+
+
+
+
+
+
+ + +
+
+
+ + + +
+
+
+
+
+
+

+ USERDEFAULTS DATA +

+
+ {% if userdefaults %} + + + + + + + + + {% for k, v in userdefaults.items %} + + + + + {% endfor %} + +
KEYVALUE
+ {{k}} + + {{v | base64_decode }} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ KEYCHAIN DATA +

+
+ {% if keychain %} + + + + + + + + + + + + {% for item in keychain %} + + + + + + + {% endfor %} + +
ITEMDATACREATE DATEMODIFICATION DATE
+ Entitlement Group: {{item.entitlement_group}}
+ Item Class: {{item.item_class}}
+ Accessible Attribute: {{item.accessible_attribute}}
+ Generic: {{item.generic}}
+ Service: {{item.service}}
+ Account: {{item.account}}
+
+ Protected: {{item.protected}}
+ Label: {{item.label}}
+ Access Control: {{item.access_control}}
+ Description: {{item.description}}
+ Comment: {{item.comment}}
+ Creator: {{item.creator}}
+ Type: {{item.type}}
+ Script Code: {{item.script_code}}
+ Alias: {{item.alias}}
+ Invisible: {{item.invisible}}
+ Negative: {{item.negative}}
+ Custom Icon: {{item.custom_icon}}
+
+
{{item.data | pretty_json | base64_decode}}
+
+ {{item.create_date}} + + {{item.modification_date}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ FILE ACCESS +

+
+ {% if files %} + + + + + + + + + {% for item in files %} + + + + + {% endfor %} + +
FILE PATHDOWNLOAD
+ {{item}} + + +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + + +
+
+
+
+
+
+

+ APP DATA DIRECTORY +

+
+ {% if datadir %} + + + + + + + + + + {% for item in datadir %} + + + + + + {% endfor %} + +
FILE PATHDOWNLOADFILE PROTECTION
+ {{ item.path | replace:"/private/var/mobile/Containers/Data/Application/|" }} + + + + {{ item.fileProtectionKey }} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ URLS INVOKED +

+
+ {% if network %} + + + + + + + + + {% for item in network %} + + + + + {% endfor %} + +
SOURCEURL
+ {{item.source}} + + {{item.url}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ JSON DATA +

+
+ {% if json %} + + + + + + + + {% for item in json %} + + + + {% endfor %} + +
JSON
+
{{item | pretty_json}}
+
+ {% endif %} +
+
+
+
+ +
+
+
+ + + + +
+
+
+
+
+
+

+ APP LOGS +

+
+ {% if logs %} + + + + + + + + {% for item in logs %} + + + + {% endfor %} + +
LOGS
+ {{item}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ TEXT INPUTS +

+
+ {% if textinputs %} + + + + + + + + {% for item in textinputs %} + + + + {% endfor %} + +
KEYSTROKES
+ {{item}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ PASTEBOARD +

+
+ {% if pasteboard %} + + + + + + + + {% for item in pasteboard %} + + + + {% endfor %} + +
ITEMS IN PASTEBOARD
+ {{item}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ APP COOKIES +

+
+ {% if cookies %} + + + + + + + + + + + + + + + {% for item in cookies %} + + + + + + + + + + + + {% endfor %} + +
NAMEVALUEDOMAINPATHEXPIRYHTTPONLYSECUREVERSION
+ {{item.name}} + + {{item.value}} + + {{item.domain}} + + {{item.path}} + + {{item.expiry}} + + {{item.httponly}} + + {{item.secure}} + + {{item.version}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ CRYPTO OPERATIONS +

+
+ {% if crypto %} + + + + + + + {% for item in crypto %} + + {% for k, v in item.items %} + {% if v %} + + {% endif %} + {% endfor %} + + {% endfor %} + + +
+ {{ k }}:
 {{ v | base64_decode | pretty_json}}
+
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ CREDENTIAL STORAGE +

+
+ {% if credentials %} + + + + + + + + + + + + + {% for item in credentials %} + + + + + + + + + {% endfor %} + +
HOSTAUTHENTICATION METHODPROTOCOLPORTUSERPASSWORD
+ {{item.host}} + + {{item.authenticationMethod}} + + {{item.protocol}} + + {{item.port}} + + {{item.user}} + + {{item.password}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + +
+
+
+
+
+
+

+ SQLITE QUERIES +

+
+ {% if sql %} + + + + + + + + {% for item in sql %} + + + + {% endfor %} + +
QUERIES
+ {{item}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + + + +
+
+
+
+
+
+

+ SCREENSHOTS +

+
+ {% for i in screenshots %} + Screenshot + {% endfor %} +
+ +
+
+
+ +
+
+
+ + + + +
+
+
+
+
+
+

+ SERVER LOCATIONS +

+
+
+
+ +
+ {% if domains %} +


This app may communicate with the following OFAC sanctioned list of countries.

+ + + + + + + + + {% for domain, details in domains.items %} + {% if details|key:"ofac" == True %} + + + + {% endif %} + {% endfor %} + +
DOMAINCOUNTRY/REGION
{{domain}} + IP: {{details|key:"geolocation"|key:"ip"}}
+ Country: {{details|key:"geolocation"|key:"country_long"}}
+ Region: {{details|key:"geolocation"|key:"region"}}
+ City: {{details|key:"geolocation"|key:"city"}}
+
+ {% endif %} +
+ +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ DOMAIN MALWARE CHECK +

+
+ {% if domains %} + + + + + + + + + + {% for domain, details in domains.items %} + + + + {% endfor %} + +
DOMAINSTATUSGEOLOCATION
{{domain}} + + {% if details|key:"bad" == "yes" %} + malware
+
+                      URL: {{details|key:"domain_or_url"}}
+                      IP: {{details|key:"ip"}}
+                      Description: {{details|key:"desc"}}
+                      
+ {% else %} + good
+ {% endif %} +
+ {% if details|key:"geolocation" %} + IP: {{details|key:"geolocation"|key:"ip"}}
+ Country: {{details|key:"geolocation"|key:"country_long"}}
+ Region: {{details|key:"geolocation"|key:"region"}}
+ City: {{details|key:"geolocation"|key:"city"}}
+ Latitude: {{details|key:"geolocation"|key:"latitude"}}
+ Longitude: {{details|key:"geolocation"|key:"longitude"}}
+ View: Google Map + + {% else %} + No Geolocation information available. + {% endif %} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ URLS +

+ {% if urls %} +
+ {% for f in urls %} + {{ f}}
+ {% endfor %} + + {% endif %} +
+
+
+
+ +
+
+ + +
+
+
+
+
+
+

+ EMAILS +

+ {% if emails %} +
+ {% for f in emails %} + {{ f}}
+ {% endfor %} + + {% endif %} +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ TRACKERS +

+
+ {% if trackers %} + + + + + + + + + + {% for trk in trackers|key:"trackers" %} + + + + + + {% endfor %} + +
TRACKER NAMECATEGORIESURL
+ {{trk.name}} + + {{trk.categories}} + + {{trk.url}} +
+ {% endif %} +
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ SQLITE DATABASE +

+
+ + + + + + + + {% for file in sqlite %} + + {% endfor %} + +
FILES
{{file|key:"file"}}
+
+
+
+
+ +
+
+
+ + +
+
+
+
+
+
+

+ PLIST FILES +

+
+ + + + + + + + {% for file in plist %} + + {% endfor %} + +
FILES
{{file|key:"file"}}
+
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+

+ OTHER FILES +

+
+ + + + + + + + {% for file in others %} + + {% endfor %} + +
FILES
{{file|key:"file"}}
+
+
+
+
+
+ +
+
+ + + + +
+ + + + + + +{% endblock %} + +{% block extra_scripts %} + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/dynamic_analysis/ios/device/system_logs.html b/mobsf/templates/dynamic_analysis/ios/device/system_logs.html new file mode 100644 index 00000000..75a58c24 --- /dev/null +++ b/mobsf/templates/dynamic_analysis/ios/device/system_logs.html @@ -0,0 +1,244 @@ +{% extends "base/base_layout.html" %} + {% block sidebar_option %} + sidebar-collapse +{% endblock %} +{% block content %} + +
+
+
+
+
+
+
+
+ + +
+
+
+ + +
+
+
+
Data refreshed in every 10 seconds.
+
+
+

+          
+
+
+
+
+
+{% endblock %} +{% block extra_scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/mobsf/templates/dynamic_analysis/ios/dynamic_analysis.html b/mobsf/templates/dynamic_analysis/ios/dynamic_analysis.html index 9cd1c565..0d19dbd1 100644 --- a/mobsf/templates/dynamic_analysis/ios/dynamic_analysis.html +++ b/mobsf/templates/dynamic_analysis/ios/dynamic_analysis.html @@ -393,7 +393,7 @@ function list_apps(){ else order = 'last'; - var url = `{% url 'dynamic_analyzer_ios'%}?bundleid=${escapeHtml(bundle)}&instance_id=${$('#ios_dynamic').val()}`; + var url = `{% url 'dynamic_analyzer_ios'%}?bundle_id=${escapeHtml(bundle)}&instance_id=${$('#ios_dynamic').val()}`; $('#in_device > tbody:' + order + '-child').append( `
diff --git a/mobsf/templates/dynamic_analysis/ios/dynamic_analyzer.html b/mobsf/templates/dynamic_analysis/ios/dynamic_analyzer.html index 6bf436c3..21da1aae 100644 --- a/mobsf/templates/dynamic_analysis/ios/dynamic_analyzer.html +++ b/mobsf/templates/dynamic_analysis/ios/dynamic_analyzer.html @@ -13,7 +13,7 @@