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 e604db97c0..1b6f976abf 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 6a26cf9ab0..54554e77a3 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 217f459fcc..f489e0fcd5 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 3971d6780d..217110efa1 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 d1b7827739..ae59e1f279 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 0b97998426..5e1bb95b51 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 09310adeb0..f409a737d1 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 35cad3675e..5963f98898 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 ffcb179097..6c75613f80 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}`); -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"); + 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 hook_all_methods_of_specific_class(className_arg) -{ - try { - setImmediate(run_hook_all_methods_of_specific_class,[className_arg]) - } catch(err) {} +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(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 a0b9cfede2..fdc880dfa4 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") - 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); +function run_show_app_classes_methods_only() { + send("Started: Find App's Classes and Methods"); + + 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}`); + + 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}`); + } } - catch(err) {} } + + free(pClasses); + } catch (err) { + send(`❌ Error: ${err.message}`); } - free(pClasses) - send("App Classes found: " + count); - send("Completed: Find App's Classes") + + send("Completed: Find App's Classes"); } -function show_app_classes_methods_only() -{ +function show_app_classes_methods_only() { try { - setImmediate(run_show_app_classes_methods_only) - } catch(err) {} + setImmediate(run_show_app_classes_methods_only); + } catch (err) { + send(`❌ Unexpected error: ${err.message}`); + } } -show_app_classes_methods_only() +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 0cbdb14de6..ceca8c9a4e 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 78d2c89c64..44892b2d5f 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 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); +function run_show_classes_with_method(pattern) { + send(`Started: Find App's Classes with method matching pattern '${pattern}'`); + + 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 cca064b105..cdabd0abb0 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 dumpMethodsOfClass(className) { + if (!ObjC.available) { + send("❌ Objective-C runtime not available"); + return; + } + + try { + const klass = ObjC.classes[className]; + if (!klass) { + send(`❌ Class not found: ${className}`); + return; + } -function run_get_app_methods_in_class() -{ - var targetClass = '{{CLASS}}'; - var found = false; - send("Looking for methods in: " + targetClass) + send(`✅ Found class: ${className}`); + const methods = klass.$methods; // includes inherited and static 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] - 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) {} - } + if (methods.length === 0) { + send(`[AUXILIARY] ⚠️ No methods found for class ${className}`); + return; } - } - free(pClasses) - if (!found) - { - send("Class not found: " + targetClass) - } else - { - send("Completed Enumerating Methods in Class: " + targetClass) + + 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 ca4bbaa870..78c3a4c58c 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) -{ - 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]); +function findClasses(pattern) { + const foundClasses = []; + + if (!ObjC.available) { + send("❌ Objective-C runtime is not available."); + return foundClasses; + } + + 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); } - free(pClasses) + return foundClasses; } +function getMatches() { + try { + const pattern = /{{PATTERN}}/i; // Replace this with your actual search + send("🔍 Searching for class names matching pattern: " + pattern); + + const matches = findClasses(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; + 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 6ddb379eae..452fe03fef 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 4869ba7a8c..08c94560f3 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) {} + setImmediate(captureStringCompare); +} catch (err) { + send("❌ Error scheduling captureStringCompare: " + err.message); +} try { - captureStringCompare2(); -} catch(err) {} \ No newline at end of file + 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 0813f2818e..1f18f7d8cd 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 dfbbe65911..148c26a587 100644 --- a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/data-dir.js +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/data-dir.js @@ -26,13 +26,14 @@ function listHomeDirectoryContents() { 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 ''; + var attributeDict = fileManager.attributesOfItemAtPath_error_(urlPath.path(), NULL); + if (attributeDict) { + var protString = attributeDict.objectForKey_("NSFileProtectionKey"); + if (protString) { + return protString.UTF8String(); + } } + return ''; } function getDataProtectionKeysForAllPaths() { diff --git a/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/json.js b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/dump/json.js index 91d92646f3..e34d809873 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 2db7274796..e4443088a3 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 c805b2a91c..396ed90bd6 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 33275fbf19..6785d76282 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.indexOf('Flutter') > -1) { + 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 2ef0cc82af..0000000000 --- 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 58dbd52dcc..0000000000 --- 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 04d5b0d4eb..0000000000 --- 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 d59cea76e3..0000000000 --- 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 30b651139a..dd0547b7f3 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,50 @@ -/* +/* 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"); - - 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:"]; - - Interceptor.replace(hook.implementation, new NativeCallback(function() - { - console.log("Hooking TrustKit"); +function bypassTrustKit() { + if (!ObjC.available) { + send("❌ Objective-C Runtime is not available!"); + return; + } + + send("🔐 SSLUnPinning Enabled"); + + try { + const classMap = ObjC.enumerateLoadedClassesSync(); + let found = false; + + for (const image in classMap) { + for (const className of classMap[image]) { + if (className === "TrustKit") { + found = true; + + 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', [])); + } } } + + if (!found) { + 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 0000000000..0875fa91ec --- /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 0000000000..e0df943eb3 --- /dev/null +++ b/mobsf/DynamicAnalyzer/tools/frida_scripts/ios/others/detect-network.js @@ -0,0 +1,27 @@ +function safeToString(obj) { + return (obj && obj.toString) ? obj.toString() : "null"; +} + + +var host = ObjC.classes.NSHost.currentHost(); +send("Host name: " + safeToString(host.name())); + +var addresses = host.addresses(); +for (var i = 0; i < addresses.count(); i++) { + send("Address: " + safeToString(addresses.objectAtIndex_(i))); +} + +var networkInfo = ObjC.classes.CTTelephonyNetworkInfo.alloc().init(); +var providers = networkInfo.serviceSubscriberCellularProviders(); +var keys = providers.allKeys(); + +for (var i = 0; i < keys.count(); i++) { + var key = keys.objectAtIndex_(i); + var 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 332c15f745..a5aa3be403 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 7c4dfb9a99..f579c52599 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 a6d91d4ceb..98185077a0 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 3e3b982a45..6486f4e60c 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 af91841d9d..5d7de495ae 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 0000000000..cba9e22971 --- /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 0000000000..4d72b3ec6d --- /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 0000000000..e57eac5a55 Binary files /dev/null and b/mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb differ diff --git a/mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm64.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb b/mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm64.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb new file mode 100644 index 0000000000..09cb7b9576 Binary files /dev/null and b/mobsf/DynamicAnalyzer/tools/ios/appsync/ai.akemi.appsyncunified_116.0_iphoneos-arm64.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb differ diff --git a/mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.1-8_iphoneos-arm.deb b/mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.1-8_iphoneos-arm.deb new file mode 100644 index 0000000000..7ca536aa16 Binary files /dev/null and b/mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.1-8_iphoneos-arm.deb differ 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 0000000000..1c2af58a44 Binary files /dev/null and b/mobsf/DynamicAnalyzer/tools/ios/oslog/oslog_0.0.4.3_iphoneos-arm64.deb differ diff --git a/mobsf/DynamicAnalyzer/views/android/environment.py b/mobsf/DynamicAnalyzer/views/android/environment.py index 340990d4de..43846a637c 100644 --- a/mobsf/DynamicAnalyzer/views/android/environment.py +++ b/mobsf/DynamicAnalyzer/views/android/environment.py @@ -24,8 +24,8 @@ 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 @@ def frida_setup(self): ' 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 ' 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 8c18ebc25e..0000000000 --- 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 aa0acd16c4..368702512f 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(f"Failed to parse binary plist: {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 0000000000..37350efba5 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/common/frida/server_update.py @@ -0,0 +1,125 @@ +# -*- 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 binary: %s', 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 Binary') + # 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 binary not found for platform: %s, architecture: %s', + self.platform, arch) + + except Exception: + logger.exception('[ERROR] Fetching Frida Server Release') + + return False diff --git a/mobsf/DynamicAnalyzer/views/common/shared.py b/mobsf/DynamicAnalyzer/views/common/shared.py index dab7015be3..06def2d43f 100644 --- a/mobsf/DynamicAnalyzer/views/common/shared.py +++ b/mobsf/DynamicAnalyzer/views/common/shared.py @@ -56,15 +56,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_type, exc_value, exc_traceback = 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: @@ -82,7 +82,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 bb1bb4815f..4a18f2913c 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 0000000000..ccaa228867 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/connect.py @@ -0,0 +1,399 @@ +# -*- coding: utf_8 -*- +"""Connect to iOS device using SSH over USB or WiFi.""" + +import logging +import paramiko +import socket +import time +import subprocess +import threading + +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 + + 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=None, username='root', password='alpine', 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, port=22, username='root', password='alpine'): + """Connect to iOS device using WiFi SSH.""" + try: + if port not in range(1, 65535): + 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 + + if self.connection_type == 'usb': + subprocess.run(['pkill', '-f', 'iproxy'], + capture_output=True) + 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") + # 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=22, username='root', password='alpine', 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 + else: + logger.warning("Not connected as root: %s", output) + return False + + except Exception as e: + logger.error("SSH connection test failed: %s", str(e)) + 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 + 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)) + subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # 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: + 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 0000000000..5ef277cf85 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/device.py @@ -0,0 +1,470 @@ +# -*- 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 (iOS applications) + 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.""" + check_install = '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 + + # Install AppSync Unified + logger.info('AppSync Unified is not installed. ' + 'Attempting to install...') + # Method 1: Install AppSync Unified from deb file + tools_dir = Path(settings.TOOLS_DIR) / 'ios' / 'appsync' + if 'arm64' in self.get_platform_architecture(): + deb_file = tools_dir / 'ai.akemi.appsyncunified_116.0_iphoneos-arm64.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb' + else: + deb_file = tools_dir / 'ai.akemi.appsyncunified_116.0_iphoneos-arm.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb' + 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): + self.execute_command('launchctl reboot userspace') + time.sleep(15) + return True + # Method 2: Install AppSync Unified 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/ ./' + install_cmds = [ + f'grep -qxF \'{src}\' {src_file} || echo \'{src}\' >> {src_file}', + 'apt update', + 'apt install -y --allow-unauthenticated ai.akemi.appinst', + 'launchctl reboot userspace', + ] + for i in install_cmds: + out, _, _ = self.execute_command(i) + logger.info(out) + logger.info('Please wait for 15 seconds for the userspace to reboot.') + 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 is installed, please try again.') + 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") + classify = lambda path: "System" if path.startswith("/Applications/") else ("User" if "/var/containers/Bundle/Application" in path else "Other") + checksum = lambda path: hashlib.md5(path.encode('utf-8')).hexdigest() + bundle_ids = [] + 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 \ No newline at end of file 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 0000000000..471a2b20f9 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/dynamic_analyzer.py @@ -0,0 +1,491 @@ +# -*- coding: utf_8 -*- +"""iOS Dynamic Analysis.""" +import logging +import os +import time +from threading import 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, + parse_host_port, + print_n_send_error_response, + strict_package_check, + is_md5, + get_md5, +) + +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 ( + get_local_ipa_list, + configure_proxy, +) +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: + 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 + Thread(target=download_screenshot, args=(ios_device, md5_hash), 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'] = f'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): + """Download screenshot from device.""" + try: + 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' + + connector = IOSConnector() + if usb: + connected = connector.connect_usb(device_id) + 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, 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 0000000000..4ee985d8fd --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/environment.py @@ -0,0 +1,317 @@ +# -*- coding: utf_8 -*- +"""iOS Device Environment for Dynamic Analysis.""" + +import logging +import plistlib +import tempfile +import os +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 on iOS device') + + # 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 binary to device') + + success = self.ios_device.install_deb('/tmp/fd.deb') + if not success: + raise Exception('Failed to install Frida server on device') + return True + + except Exception: + logger.exception('[ERROR] Installing Frida server on iOS device') + 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 the current plist content + output = self.ios_device.read_file(self.frida_plist_path) + if not output: + raise Exception('Failed to read Frida plist') + + if "-l" in output and "0.0.0.0" in output: + logger.info('WiFi arguments already present in plist') + return True + + with tempfile.NamedTemporaryFile(mode='wb', delete=False) as temp_file: + temp_file.write(output.encode('utf-8')) + temp_file_path = temp_file.name + + try: + # Load the existing plist + with open(temp_file_path, "rb") as f: + plist = plistlib.load(f) + + # Get current ProgramArguments + args = plist.get("ProgramArguments", []) + + # Add WiFi arguments if not present + if "-l" not in args: + args.append("-l") + if "0.0.0.0" not in args: + args.append("0.0.0.0") + + # Update the plist + plist["ProgramArguments"] = args + logger.info('Added -l 0.0.0.0 to ProgramArguments') + + # Save the updated plist to temporary file + with open(temp_file_path, "wb") as f: + plistlib.dump(plist, f, fmt=plistlib.FMT_XML) + + self.ios_device.upload_file(temp_file_path, self.frida_plist_path) + + finally: + # Clean up temporary file + if os.path.exists(temp_file_path): + os.unlink(temp_file_path) + + 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 on 0.0.0.0') + 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 on (0.0.0.0)') + return True + + except Exception: + logger.exception('[ERROR] Enabling Frida Server on 0.0.0.0') + 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 on (0.0.0.0)') + 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 + success = FridaServerUpdater(platform, frida_version).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) + 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: + # Atempt to start the server using the binary + _, error, exit_code = self.ios_device.execute_command( + f'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 0000000000..17466466f8 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/frida.py @@ -0,0 +1,164 @@ +"""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 + +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 + 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('Spawned %s with PID %s', self.bundle_id, pid) + return pid + except frida.NotSupportedError: + logger.error(self.not_supported_text) + logger.info('Spawned %s with PID %s', 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('Spawned %s with PID %s', 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('Spawned %s with PID %s', self.bundle_id, _PID) + #time.sleep(2) + 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 0000000000..0108e8e460 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/device/report.py @@ -0,0 +1,308 @@ +# -*- 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 ( + validate_and_connect_device, + OK, + FAILED, +) +from mobsf.DynamicAnalyzer.tools.webproxy import ( + get_http_tools_url, + stop_httptools, +) +from mobsf.MobSF.utils import ( + base64_decode, + is_md5, + get_md5, + key, + pretty_json, + print_n_send_error_response, + replace, + strict_package_check, + IOS_DEVICE_ID_REGEX, + SSH_DEVICE_ID_REGEX, +) +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) + fo = ios_device.download_file_object(remote_path) + if fo: + response = HttpResponse(fo, 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 2119ea8c74..3ecd982307 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 @@ 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 ( + get_local_ipa_list, + configure_proxy, +) 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 @@ 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 092e59290e..f0d9b31883 100644 --- a/mobsf/DynamicAnalyzer/views/ios/frida_core.py +++ b/mobsf/DynamicAnalyzer/views/ios/frida_core.py @@ -287,8 +287,15 @@ def api_handler(self, api): 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 0000000000..fa30c3cd50 --- /dev/null +++ b/mobsf/DynamicAnalyzer/views/ios/helpers.py @@ -0,0 +1,295 @@ +# -*- 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 a57846bebb..3c090c83a9 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/security.py b/mobsf/MobSF/security.py index 24b730e09f..f96ae706cd 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 5e338f44c4..00615fe97b 100644 --- a/mobsf/MobSF/settings.py +++ b/mobsf/MobSF/settings.py @@ -532,6 +532,12 @@ # if VT_UPLOAD is set to True. # =============================================== # =======IOS DYNAMIC ANALYSIS SETTINGS=========== + # Should be SSH IP:PORT, example: 192.168.1.100:22 + # Filed also supports multiple devices, example: 192.168.1.100:22,192.168.1.101:22 + IOS_ANALYZER_IDENTIFIERS = os.getenv('MOBSF_IOS_ANALYZER_IDENTIFIERS', '') + # ============================================== + + # =======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 5e7330d489..d3f35273d3 100755 --- a/mobsf/MobSF/urls.py +++ b/mobsf/MobSF/urls.py @@ -15,6 +15,12 @@ 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, @@ -123,7 +129,7 @@ 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$', @@ -399,7 +405,46 @@ 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 6015d2063e..ea6d989610 100755 --- a/mobsf/MobSF/utils.py +++ b/mobsf/MobSF/utils.py @@ -62,7 +62,8 @@ 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): GREEN = '\033[92m' @@ -998,3 +999,15 @@ 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") \ No newline at end of file diff --git a/mobsf/static/others/css/spinner.css b/mobsf/static/others/css/spinner.css index bc0357e781..9c11a75779 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 9c0e66dad3..8c17bd056f 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 0000000000..5254ae86ab --- /dev/null +++ b/mobsf/templates/dynamic_analysis/ios/device/dynamic_analyzer.html @@ -0,0 +1,1064 @@ + +{% 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 0000000000..304cb10d41 --- /dev/null +++ b/mobsf/templates/dynamic_analysis/ios/device/dynamic_report.html @@ -0,0 +1,1484 @@ +{% 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 0000000000..75a58c2495 --- /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 9cd1c5651e..0d19dbd160 100644 --- a/mobsf/templates/dynamic_analysis/ios/dynamic_analysis.html +++ b/mobsf/templates/dynamic_analysis/ios/dynamic_analysis.html @@ -393,7 +393,7 @@ 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 6bf436c3ed..04766ef975 100644 --- a/mobsf/templates/dynamic_analysis/ios/dynamic_analyzer.html +++ b/mobsf/templates/dynamic_analysis/ios/dynamic_analyzer.html @@ -13,7 +13,7 @@