Jailbroken iOS Device Support (#2536)

Enables dynamic analysis on real jailbroken iOS devices connected via USB or WiFi SSH, complementing the existing Corellium-based iOS analysis.              
                                                                                                                                                                                                                                                                                                                                                                                                                                                                
  - SSH connectivity — connects to jailbroken devices over USB (via iproxy port forwarding) or WiFi using Paramiko SSH                                         
  - Frida instrumentation — spawns/attaches Frida on-device, with full hook support matching the existing Corellium flow                                       
  - Environment setup — installs AppSync Unified and Frida server on first run; supports both arm and arm64 devices                                            
  - Dynamic analysis — app file extraction, system log streaming (oslog), screenshot capture, process listing, and report generation
  - SSH terminal — execute shell commands on the device over SSH

---------

Co-authored-by: CylentSec <[email protected]>
Co-authored-by: Oz <[email protected]>
This commit is contained in:
Ajin Abraham
2026-03-23 01:46:03 -07:00
committed by GitHub
co-authored by CylentSec Oz
parent 6f8a43c1b7
commit 6e875fb77b
76 changed files with 8669 additions and 925 deletions
@@ -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;
@@ -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);
@@ -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)
@@ -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;
@@ -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);
}
});
@@ -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) {}
@@ -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");
@@ -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;
}
@@ -1,49 +1,82 @@
/* Description: Hook all the methods of a particular class
* Mode: S+A
* Version: 1.0
* Credit: https://github.com/interference-security/frida-scripts/blob/master/iOS
* Author: @interference-security
*/
//Twitter: https://twitter.com/xploresec
//GitHub: https://github.com/interference-security
function hook_class_method(class_name, method_name)
{
var hook = ObjC.classes[class_name][method_name];
Interceptor.attach(hook.implementation, {
onEnter: function(args) {
send("[AUXILIARY] Detected call to: " + class_name + " -> " + method_name);
}
});
function hook_class_method(className, methodName) {
try {
const method = ObjC.classes[className][methodName];
const impl = method?.implementation;
if (!impl || impl.isNull()) {
send(`⚠️ Skipping ${methodName} — no implementation.`);
return;
}
Interceptor.attach(impl, {
onEnter: function (args) {
send(`[AUXILIARY] Detected call to: ${className} -> ${methodName}`);
for (let i = 0; i < method.argumentTypes.length; i++) {
try {
const arg = args[i];
if (arg.isNull && arg.isNull()) {
send(`[AUXILIARY] arg[${i}]: null`);
} else {
try {
const str = arg.readUtf8String();
send(`[AUXILIARY] arg[${i}]: "${str}"`);
} catch {
send(`[AUXILIARY] arg[${i}]: ${arg}`);
}
}
} catch (err) {
send(`[AUXILIARY] arg[${i}]: <error reading: ${err.message}>`);
}
}
},
onLeave: function (retval) {
try {
if (retval.isNull && retval.isNull()) {
send(`[AUXILIARY] return: null`);
} else {
send(`[AUXILIARY] return: ${retval}`);
}
} catch (err) {
send(`[AUXILIARY] return: <error reading: ${err.message}>`);
}
}
});
} catch (err) {
send(`❌ Failed to hook ${className} -> ${methodName}: ${err.message}`);
}
}
function run_hook_all_methods_of_specific_class(className_arg)
{
send("Started: Hook all methods of a specific class");
send("Class Name: " + className_arg);
//Your class name here
var className = className_arg;
//var methods = ObjC.classes[className].$methods;
var methods = ObjC.classes[className].$ownMethods;
for (var i = 0; i < methods.length; i++)
{
send("[AUXILIARY] [-] "+methods[i]);
send("[AUXILIARY] \t[*] Hooking into implementation");
//eval('var className2 = "'+className+'"; var funcName2 = "'+methods[i]+'"; var hook = eval(\'ObjC.classes.\'+className2+\'["\'+funcName2+\'"]\'); Interceptor.attach(hook.implementation, { onEnter: function(args) { console.log("[*] Detected call to: " + className2 + " -> " + funcName2); } });');
var className2 = className;
var funcName2 = methods[i];
hook_class_method(className2, funcName2);
// send("[AUXILIARY] \t[*] Hooking successful");
}
send("[AUXILIARY] Completed: Hook all methods of a specific class");
function run_hook_all_methods_of_specific_class(className) {
send(`📦 Started hooking all methods of: ${className}`);
try {
if (!ObjC.classes.hasOwnProperty(className)) {
send(`❌ Class not found: ${className}`);
return;
}
const methods = ObjC.classes[className].$ownMethods;
for (const methodName of methods) {
send(`[AUXILIARY] Hooking: ${methodName}`);
hook_class_method(className, methodName);
}
send("✅ Completed hooking all methods.");
} catch (err) {
send(`❌ Error while hooking class: ${err.message}`);
}
}
function hook_all_methods_of_specific_class(className_arg)
{
try {
setImmediate(run_hook_all_methods_of_specific_class,[className_arg])
} catch(err) {}
function hook_all_methods_of_specific_class(classNameArg) {
try {
setImmediate(run_hook_all_methods_of_specific_class, classNameArg);
} catch (err) {
send(`❌ Unexpected error: ${err.message}`);
}
}
//Your class name goes here
hook_all_methods_of_specific_class('{{CLASS}}')
// Replace '{{CLASS}}' with the class name you want to hook
hook_all_methods_of_specific_class('{{CLASS}}');
@@ -1,54 +1,68 @@
/* Description: Dump all methods inside classes owned by the app only
* Mode: S+A
* Version: 1.0
* Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS
* Author: @interference-security
*/
//Twitter: https://twitter.com/xploresec
//GitHub: https://github.com/interference-security
// Modified to support Frida 17.0.0+
function run_show_app_classes_methods_only() {
send("Started: Find App's Classes and Methods");
function run_show_app_classes_methods_only()
{
send("Started: Find App's Classes and Methods")
var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer'])
var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer'])
var p = Memory.alloc(Process.pointerSize)
p.writeUInt(0)
var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String()
var pPath = Memory.allocUtf8String(path)
var pClasses = copyClassNamesForImage(pPath, p)
var count = p.readUInt()
var classesArray = new Array(count)
for (var i = 0; i < count; i++)
{
var pClassName = pClasses.add(i * Process.pointerSize).readPointer()
classesArray[i] = pClassName.readUtf8String()
var className = classesArray[i]
send("[AUXILIARY] Class: " + className);
//var methods = ObjC.classes[className].$methods;
var methods = ObjC.classes[className].$ownMethods;
for (var j = 0; j < methods.length; j++)
{
send("[AUXILIARY] \t[-] Method: " + methods[j]);
try
{
send("[AUXILIARY] \t\t[-] Arguments Type: " + ObjC.classes[className][methods[j]].argumentTypes);
send("[AUXILIARY] \t\t[-] Return Type: " + ObjC.classes[className][methods[j]].returnType);
}
catch(err) {}
}
}
free(pClasses)
send("App Classes found: " + count);
send("Completed: Find App's Classes")
}
function show_app_classes_methods_only()
{
try {
setImmediate(run_show_app_classes_methods_only)
} catch(err) {}
const free = new NativeFunction(
Process.getModuleByName('libc++abi.dylib').getExportByName('free'),
'void',
['pointer']
);
const copyClassNamesForImage = new NativeFunction(
Process.getModuleByName('libobjc.A.dylib').getExportByName('objc_copyClassNamesForImage'),
'pointer',
['pointer', 'pointer']
);
const path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String();
const pPath = Memory.allocUtf8String(path);
const pCount = Memory.alloc(Process.pointerSize);
pCount.writeUInt(0);
const pClasses = copyClassNamesForImage(pPath, pCount);
const count = pCount.readUInt();
send(`[AUXILIARY] Classes found in app image: ${count}`);
for (let i = 0; i < count; i++) {
const classPtr = pClasses.add(i * Process.pointerSize).readPointer();
const className = classPtr.readUtf8String();
if (!ObjC.classes.hasOwnProperty(className)) {
continue;
}
const klass = ObjC.classes[className];
send(`[AUXILIARY] Class: ${className}`);
const methods = klass.$ownMethods;
for (const methodName of methods) {
send(` [-] Method: ${methodName}`);
try {
const method = klass[methodName];
send(` ├─ Args: ${method.argumentTypes}`);
send(` └─ Return: ${method.returnType}`);
} catch (err) {
send(` ⚠️ Failed to inspect method: ${err.message}`);
}
}
}
free(pClasses);
} catch (err) {
send(`❌ Error: ${err.message}`);
}
send("Completed: Find App's Classes");
}
show_app_classes_methods_only()
function show_app_classes_methods_only() {
try {
setImmediate(run_show_app_classes_methods_only);
} catch (err) {
send(`❌ Unexpected error: ${err.message}`);
}
}
show_app_classes_methods_only();
@@ -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);
@@ -1,55 +1,76 @@
/* Description: Find a specific method in all classes in the app
* Modified for MobSF
* Mode: S+A
* Version: 1.0
* Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS
* Author: @interference-security
*/
//Twitter: https://twitter.com/xploresec
//GitHub: https://github.com/interference-security
// Modified to support Frida 17.0.0+
function run_show_classes_with_method(pattern) {
send(`Started: Find App's Classes with method matching pattern '${pattern}'`);
function find_specific_method_in_all_classes(func_name)
{
send("Searching for method [" + func_name + "] in all Classes");
var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer'])
var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer'])
var p = Memory.alloc(Process.pointerSize)
p.writeUInt(0)
var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String()
var pPath = Memory.allocUtf8String(path)
var pClasses = copyClassNamesForImage(pPath, p)
var count = p.readUInt()
var classesArray = new Array(count)
for (var i = 0; i < count; i++)
{
var pClassName = pClasses.add(i * Process.pointerSize).readPointer()
classesArray[i] = pClassName.readUtf8String()
var className = classesArray[i]
//var methods = ObjC.classes[className].$methods;
var methods = ObjC.classes[className].$ownMethods;
for (var j = 0; j < methods.length; j++)
{
if(methods[j].includes(func_name))
{
send("[AUXILIARY] Class: " + className);
send("[AUXILIARY] \t[-] Method: " + methods[j]);
try
{
send("[AUXILIARY] \t\t[-] Arguments Type: " + ObjC.classes[className][methods[j]].argumentTypes);
send("[AUXILIARY] \t\t[-] Return Type: " + ObjC.classes[className][methods[j]].returnType);
try {
const free = new NativeFunction(
Process.getModuleByName('libc++abi.dylib').getExportByName('free'),
'void',
['pointer']
);
const copyClassNamesForImage = new NativeFunction(
Process.getModuleByName('libobjc.A.dylib').getExportByName('objc_copyClassNamesForImage'),
'pointer',
['pointer', 'pointer']
);
const path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String();
const pPath = Memory.allocUtf8String(path);
const pCount = Memory.alloc(Process.pointerSize);
pCount.writeUInt(0);
const pClasses = copyClassNamesForImage(pPath, pCount);
const count = pCount.readUInt();
send(`[AUXILIARY] Classes found in app image: ${count}`);
const regex = new RegExp(pattern);
for (let i = 0; i < count; i++) {
const classPtr = pClasses.add(i * Process.pointerSize).readPointer();
const className = classPtr.readUtf8String();
if (!ObjC.classes.hasOwnProperty(className)) {
continue;
}
const klass = ObjC.classes[className];
const methods = klass.$ownMethods;
const matchedMethods = methods.filter(name => regex.test(name));
if (matchedMethods.length === 0) continue;
send(`[MATCHED] Class: ${className}`);
for (const methodName of matchedMethods) {
send(` [-] Method: ${methodName}`);
try {
const method = klass[methodName];
send(` ├─ Args: ${method.argumentTypes}`);
send(` └─ Return: ${method.returnType}`);
} catch (err) {
send(` ⚠️ Failed to inspect method: ${err.message}`);
}
catch(err) {}
}
}
free(pClasses);
} catch (err) {
send(`❌ Error: ${err.message}`);
}
free(pClasses)
send("Completed: Find specific Method in all Classes");
send("Completed: Find matching classes");
}
// Usage example:
function show_classes_with_method(pattern) {
try {
setImmediate(() => run_show_classes_with_method(pattern));
} catch (err) {
send(`❌ Unexpected error: ${err.message}`);
}
}
//Your function name goes here
var METHOD = '{{METHOD}}'
try {
find_specific_method_in_all_classes(METHOD)
} catch(err) {}
// Example call:
show_classes_with_method("{{METHOD}}");
@@ -1,63 +1,29 @@
/* Description: Get all methods of the class
* Modified for MobSF
* Mode: S+A
* Version: 1.0
* Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS
* Author: @interference-security
*/
//Twitter: https://twitter.com/xploresec
//GitHub: https://github.com/interference-security
// Modified to support Frida 17.0.0+
function run_get_app_methods_in_class()
{
var targetClass = '{{CLASS}}';
var found = false;
send("Looking for methods in: " + targetClass)
var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer'])
var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer'])
var p = Memory.alloc(Process.pointerSize)
p.writeUInt(0)
var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String()
var pPath = Memory.allocUtf8String(path)
var pClasses = copyClassNamesForImage(pPath, p)
var count = p.readUInt()
var classesArray = new Array(count)
for (var i = 0; i < count; i++)
{
var pClassName = pClasses.add(i * Process.pointerSize).readPointer()
classesArray[i] = pClassName.readUtf8String()
var className = classesArray[i]
if (className === targetClass)
{
found = true;
send("[AUXILIARY] Class: " + className);
//var methods = ObjC.classes[className].$methods;
var methods = ObjC.classes[className].$ownMethods;
for (var j = 0; j < methods.length; j++)
{
send("[AUXILIARY] \t[-] Method: " + methods[j]);
try
{
send("[AUXILIARY] \t\t[-] Arguments Type: " + ObjC.classes[className][methods[j]].argumentTypes);
send("[AUXILIARY] \t\t[-] Return Type: " + ObjC.classes[className][methods[j]].returnType);
}
catch(err) {}
}
}
function dumpMethodsOfClass(className) {
if (!ObjC.available) {
send("❌ Objective-C runtime not available");
return;
}
free(pClasses)
if (!found)
{
send("Class not found: " + targetClass)
} else
{
send("Completed Enumerating Methods in Class: " + targetClass)
try {
const klass = ObjC.classes[className];
if (!klass) {
send(`❌ Class not found: ${className}`);
return;
}
send(`✅ Found class: ${className}`);
const methods = klass.$methods; // includes inherited and static methods
if (methods.length === 0) {
send(`[AUXILIARY] ⚠️ No methods found for class ${className}`);
return;
}
send(`[AUXILIARY] 🧬 Methods for ${className} (${methods.length} total):`);
methods.forEach(method => send(` [-] ${method}`));
} catch (err) {
send(`❌ Error dumping methods for ${className}: ${err.message}`);
}
}
try {
run_get_app_methods_in_class()
} catch(err) {}
setImmediate(dumpMethodsOfClass, "{{CLASS}}");
@@ -1,59 +1,50 @@
/* Description: Find classes matching a pattern
* Modified for MobSF
* Mode: S+A
* Version: 1.0
* Credit: PassionFruit (https://github.com/chaitin/passionfruit/blob/master/agent/app/classdump.js) & https://github.com/interference-security/frida-scripts/blob/master/iOS
* Author: @interference-security
*/
//Twitter: https://twitter.com/xploresec
//GitHub: https://github.com/interference-security
// Modified to support Frida 17.0.0+
function findClasses(pattern) {
const foundClasses = [];
function findClasses(pattern)
{
var foundClasses = [];
var free = new NativeFunction(Module.getGlobalExportByName('free'), 'void', ['pointer'])
var copyClassNamesForImage = new NativeFunction(Module.getGlobalExportByName('objc_copyClassNamesForImage'), 'pointer', ['pointer', 'pointer'])
var p = Memory.alloc(Process.pointerSize)
p.writeUInt(0)
var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String()
var pPath = Memory.allocUtf8String(path)
var pClasses = copyClassNamesForImage(pPath, p)
var count = p.readUInt()
var classesArray = new Array(count)
for (var i = 0; i < count; i++)
{
var pClassName = pClasses.add(i * Process.pointerSize).readPointer()
classesArray[i] = pClassName.readUtf8String()
if (classesArray[i].match(pattern)) {
foundClasses.push( classesArray[i]);
}
if (!ObjC.available) {
send("❌ Objective-C runtime is not available.");
return foundClasses;
}
free(pClasses)
try {
const classMap = ObjC.enumerateLoadedClassesSync();
for (const imagePath in classMap) {
if (!imagePath.includes(".app")) continue; // App-only classes
for (const className of classMap[imagePath]) {
if (pattern.test(className)) {
foundClasses.push(className);
}
}
}
} catch (err) {
send("❌ Error during class enumeration: " + err.message);
}
return foundClasses;
}
function getMatches() {
try {
const pattern = /{{PATTERN}}/i; // Replace this with your actual search
send("🔍 Searching for class names matching pattern: " + pattern);
function getMatches(){
var matches;
try{
var pattern = /{{PATTERN}}/i;
send('Class search for pattern: ' + pattern)
matches = findClasses(pattern);
}catch (err){
send('Class pattern match [\"Error\"] => ' + err);
return;
const matches = findClasses(pattern);
if (matches.length > 0) {
send(`✅ Found [${matches.length}] match(es):`);
matches.forEach(className => send("[AUXILIARY] " + className));
} else {
send("❌ No matches found.");
}
} catch (err) {
send("❌ Error during pattern match: " + err.message);
}
if (matches.length>0)
send('Found [' + matches.length + '] matches')
else
send('No matches found')
matches.forEach(function(clz) {
send('[AUXILIARY] ' + clz)
});
}
try {
getMatches();
} catch(err) {}
setImmediate(getMatches);
} catch (err) {
send("❌ Script scheduling failed: " + err.message);
}
@@ -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) {}
setImmediate(captureString);
} catch (err) {
send("❌ Unexpected error: " + err.message);
}
@@ -1,27 +1,66 @@
function captureStringCompare() {
send('Capturing string comparisons')
Interceptor.attach(ObjC.classes.__NSCFString['- isEqualToString:'].implementation, {
onEnter: function (args) {
var src = new ObjC.Object(ptr(args[0])).toString()
var str = new ObjC.Object(ptr(args[2])).toString()
send('[AUXILIARY] [__NSCFString isEqualToString:] -> \nstring 1: '+ src + '\nstring 2: '+ str);
if (!ObjC.available) {
send("❌ Objective-C runtime not available.");
return;
}
try {
const cls = ObjC.classes.__NSCFString;
if (!cls || !cls["- isEqualToString:"]) {
send("❌ __NSCFString or method -isEqualToString: not available.");
return;
}
});
Interceptor.attach(cls["- isEqualToString:"].implementation, {
onEnter: function (args) {
try {
const str1 = new ObjC.Object(args[0]).toString();
const str2 = new ObjC.Object(args[2]).toString();
send(`[AUXILIARY] __NSCFString -isEqualToString:\n ↳ string 1: ${str1}\n ↳ string 2: ${str2}`);
} catch (e) {
send(`⚠️ Failed to capture comparison: ${e.message}`);
}
}
});
} catch (err) {
send("❌ Hooking __NSCFString failed: " + err.message);
}
}
function captureStringCompare2(){
Interceptor.attach(ObjC.classes.NSTaggedPointerString['- isEqualToString:'].implementation, {
onEnter: function (args) {
var src = new ObjC.Object(ptr(args[0])).toString()
var str = new ObjC.Object(ptr(args[2])).toString()
send('[AUXILIARY] NSTaggedPointerString[- isEqualToString:] -> \nstring 1: '+ src + '\nstring 2: '+ str);
function captureStringCompare2() {
if (!ObjC.available) return;
try {
const cls = ObjC.classes.NSTaggedPointerString;
if (!cls || !cls["- isEqualToString:"]) {
send("❌ NSTaggedPointerString or method -isEqualToString: not available.");
return;
}
});
Interceptor.attach(cls["- isEqualToString:"].implementation, {
onEnter: function (args) {
try {
const str1 = new ObjC.Object(args[0]).toString();
const str2 = new ObjC.Object(args[2]).toString();
send(`[AUXILIARY] NSTaggedPointerString -isEqualToString:\n ↳ string 1: ${str1}\n ↳ string 2: ${str2}`);
} catch (e) {
send(`⚠️ Failed to capture tagged comparison: ${e.message}`);
}
}
});
} catch (err) {
send("❌ Hooking NSTaggedPointerString failed: " + err.message);
}
}
try {
captureStringCompare();
} catch(err) {}
try {
captureStringCompare2();
} catch(err) {}
setImmediate(captureStringCompare);
} catch (err) {
send("❌ Error scheduling captureStringCompare: " + err.message);
}
try {
setImmediate(captureStringCompare2);
} catch (err) {
send("❌ Error scheduling captureStringCompare2: " + err.message);
}
@@ -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);
}
@@ -5,10 +5,10 @@
*
*/
function listDirectoryContentsAtPath(path) {
var fileManager = ObjC.classes.NSFileManager.defaultManager();
var enumerator = fileManager.enumeratorAtPath_(path);
var file;
var paths = [];
const fileManager = ObjC.classes.NSFileManager.defaultManager();
const enumerator = fileManager.enumeratorAtPath_(path);
let file;
const paths = [];
while ((file = enumerator.nextObject()) !== null) {
paths.push(path + '/' + file);
@@ -18,32 +18,33 @@ function listDirectoryContentsAtPath(path) {
}
function listHomeDirectoryContents() {
var homePath = ObjC.classes.NSProcessInfo.processInfo().environment().objectForKey_("HOME").toString();
var paths = listDirectoryContentsAtPath(homePath);
const homePath = ObjC.classes.NSProcessInfo.processInfo().environment().objectForKey_("HOME").toString();
const paths = listDirectoryContentsAtPath(homePath);
return paths;
}
function getDataProtectionKeyForPath(path) {
var fileManager = ObjC.classes.NSFileManager.defaultManager();
var urlPath = ObjC.classes.NSURL.fileURLWithPath_(path);
var fileProtectionKey = ObjC.Object(ptr(fileManager.attributesOfItemAtPath_error_(urlPath.path(), NULL)));
var protString = fileProtectionKey.valueForKey_("NSFileProtectionKey")
if (protString)
return protString.UTF8String();
else{
return '';
const fileManager = ObjC.classes.NSFileManager.defaultManager();
const urlPath = ObjC.classes.NSURL.fileURLWithPath_(path);
const attributeDict = fileManager.attributesOfItemAtPath_error_(urlPath.path(), NULL);
if (attributeDict) {
const protString = attributeDict.objectForKey_("NSFileProtectionKey");
if (protString) {
return protString.UTF8String();
}
}
return '';
}
function getDataProtectionKeysForAllPaths() {
var fileManager = ObjC.classes.NSFileManager.defaultManager();
var dict = [];
var paths = listHomeDirectoryContents();
const fileManager = ObjC.classes.NSFileManager.defaultManager();
const dict = [];
const paths = listHomeDirectoryContents();
var isDir = Memory.alloc(Process.pointerSize);
const isDir = Memory.alloc(Process.pointerSize);
isDir.writePointer(NULL);
for (var i = 0; i < paths.length; i++) {
for (let i = 0; i < paths.length; i++) {
fileManager.fileExistsAtPath_isDirectory_(paths[i], isDir);
@@ -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}));
}
});
@@ -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
@@ -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) {
}
@@ -26,18 +26,14 @@ function hook_ssl_verify_result(address)
function disablePinning()
{
var pattern = "FF 03 05 D1 FC 6F 0F A9 F8 5F 10 A9 F6 57 11 A9 F4 4F 12 A9 FD 7B 13 A9 FD C3 04 91 08 0A 80 52"
Process.enumerateRangesSync('r-x').filter(function (m)
{
if (m.file) return m.file.path.indexOf('Flutter') > -1;
return false;
}).forEach(function (r)
{
Memory.scanSync(r.base, r.size, pattern).forEach(function (match) {
send('[+] ssl_verify_result found at: ' + match.address.toString());
hook_ssl_verify_result(match.address);
send('[*] Started: Bypass Flutter SSL-Pinning');
});
});
for (const r of Process.enumerateRanges('r-x')) {
if (r.file && r.file.path.includes('Flutter')) {
for (const match of Memory.scan(r.base, r.size, pattern)) {
send('[+] ssl_verify_result found at: ' + match.address.toString());
hook_ssl_verify_result(match.address);
send('[*] Started: Bypass Flutter SSL-Pinning');
}
}
}
}
setTimeout(disablePinning, 1000)
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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 ");
@@ -1,33 +1,45 @@
/*
/*
Description: iOS TrustKit Certificate Pinning ByPass
Usage: frida -U -f XXX -l ios-trustkit-pinning-bypass.js
Credit: Unknown
Src: https://github.com/rsenet/FriList/blob/main/02_SecurityBypass/CertificatePinning/ios-trustkit-pinning-bypass.js
Updated for Frida 17+
*/
if (ObjC.available)
{
console.log("SSLUnPinning Enabled");
function bypassTrustKit() {
if (!ObjC.available) {
send("❌ Objective-C Runtime is not available!");
return;
}
for (var className in ObjC.classes)
{
if (ObjC.classes.hasOwnProperty(className))
{
if (className == "TrustKit")
{
console.log("Found our target class : " + className);
var hook = ObjC.classes.TrustKit["+ initSharedInstanceWithConfiguration:"];
send("🔐 SSLUnPinning Enabled");
Interceptor.replace(hook.implementation, new NativeCallback(function()
{
console.log("Hooking TrustKit");
try {
const classMap = ObjC.enumerateLoadedClassesSync();
for (const image in classMap) {
for (const className of classMap[image]) {
if (className === "TrustKit") {
send("✅ Found TrustKit class in: " + image);
const method = ObjC.classes.TrustKit["+ initSharedInstanceWithConfiguration:"];
if (!method || method.implementation.isNull()) {
send("❌ TrustKit method not found or invalid.");
return;
}
Interceptor.replace(method.implementation, new NativeCallback(function () {
send("✅ Hooked TrustKit: +initSharedInstanceWithConfiguration:");
return;
}, 'int', []));
send("✅ TrustKit bypass hook installed.");
return;
}, 'int', []));
}
}
}
send("❌ TrustKit class not found.");
} catch (err) {
send("❌ Error during TrustKit bypass: " + err.message);
}
}
else
{
console.log("Objective-C Runtime is not available!");
}
}
setImmediate(bypassTrustKit);
@@ -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);
@@ -0,0 +1,27 @@
function safeToString(obj) {
return obj?.toString?.() ?? "null";
}
const host = ObjC.classes.NSHost.currentHost();
send("Host name: " + safeToString(host.name()));
const addresses = host.addresses();
for (let i = 0; i < addresses.count(); i++) {
send("Address: " + safeToString(addresses.objectAtIndex_(i)));
}
const networkInfo = ObjC.classes.CTTelephonyNetworkInfo.alloc().init();
const providers = networkInfo.serviceSubscriberCellularProviders();
const keys = providers.allKeys();
for (let j = 0; j < keys.count(); j++) {
const key = keys.objectAtIndex_(j);
const carrier = providers.objectForKey_(key);
send("Carrier for " + safeToString(key) + ":");
send(" Carrier Name: " + safeToString(carrier.carrierName()));
send(" Mobile Country Code: " + safeToString(carrier.mobileCountryCode()));
send(" Mobile Network Code: " + safeToString(carrier.mobileNetworkCode()));
send(" ISO Country Code: " + safeToString(carrier.isoCountryCode()));
send(" Allows VOIP: " + carrier.allowsVOIP());
}
@@ -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()
show_classes_methods_of_app();
@@ -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()
show_classes_of_app();
@@ -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, ' '))
@@ -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, ' '));
@@ -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 = "";
@@ -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();
}
@@ -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);
}
@@ -24,8 +24,8 @@ from mobsf.DynamicAnalyzer.tools.webproxy import (
start_proxy,
stop_httptools,
)
from mobsf.DynamicAnalyzer.views.android import (
frida_server_download as fserver,
from mobsf.DynamicAnalyzer.views.common.frida.server_update import (
FridaServerUpdater,
)
from mobsf.MobSF.utils import (
get_adb,
@@ -699,7 +699,8 @@ class Environment:
' instance is running')
return
frida_bin = f'frida-server-{frida_version}-android-{frida_arch}'
stat = fserver.update_frida_server(frida_arch, frida_version)
stat = FridaServerUpdater(
'android', frida_version).update_frida_server(frida_arch)
if not stat:
msg = ('Cannot download frida-server binary. You will need'
f' {frida_bin} in {settings.DWD_DIR} for '
@@ -707,7 +708,7 @@ class Environment:
logger.error(msg)
return
frida_path = os.path.join(settings.DWD_DIR, frida_bin)
logger.info('Copying frida server for %s', frida_arch)
logger.info('Copying frida server v%s for %s', frida_version, frida_arch)
self.adb_command(['push', frida_path, '/system/fd_server'])
self.adb_command(['chmod', '755', '/system/fd_server'], True)
@@ -715,7 +716,7 @@ class Environment:
"""Start Frida Server."""
check = self.adb_command(['ps'], True)
if b'fd_server' in check:
logger.info('Frida Server is already running')
logger.info('Frida Server v%s is already running', frida_version)
return
def start_frida():
@@ -729,6 +730,6 @@ class Environment:
trd = threading.Thread(target=start_frida)
trd.daemon = True
trd.start()
logger.info('Starting Frida Server')
logger.info('Starting Frida Server v%s', frida_version)
logger.info('Waiting for 2 seconds...')
time.sleep(2)
@@ -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
+18 -3
View File
@@ -54,9 +54,24 @@ def view_file(request, api=False):
if not is_safe_path(src, sfile.as_posix(), fil):
err = 'Path Traversal Attack Detected'
return print_n_send_error_response(request, err, api)
dat = sfile.read_text('ISO-8859-1')
if fil.endswith('.plist') and dat.startswith('bplist0'):
dat = dumps(dat, fmt=FMT_XML).decode('utf-8', 'ignore')
# Check if it's a binary plist first
if fil.endswith('.plist'):
# Read as bytes to check for binary plist
raw_data = sfile.read_bytes()
if raw_data.startswith(b'bplist0'):
# It's a binary plist, load it properly
from plistlib import loads
try:
plist_data = loads(raw_data)
dat = dumps(plist_data, fmt=FMT_XML).decode('utf-8', 'ignore')
except Exception as e:
logger.warning('Failed to parse binary plist: %s', e)
dat = raw_data.decode('utf-8', 'ignore')
else:
# It's a text plist, read as text
dat = raw_data.decode('utf-8', 'ignore')
else:
dat = sfile.read_text('ISO-8859-1')
if fil.endswith(('.xml', '.plist')) and typ in ['xml', 'plist']:
rtyp = 'xml'
elif typ == 'db':
@@ -0,0 +1,128 @@
# -*- coding: utf_8 -*-
"""Common Frida Server Update Management for Android and iOS."""
import logging
from pathlib import Path
from lzma import LZMAFile
from shutil import copyfileobj
import requests
from django.conf import settings
from mobsf.MobSF.utils import (
is_internet_available,
upstream_proxy,
)
logger = logging.getLogger(__name__)
class FridaServerUpdater:
"""Class for managing Frida server updates and downloads."""
def __init__(self, platform, version):
"""Initialize the FridaServerUpdater.
Args:
platform (str): The platform ('android' or 'ios')
version (str): The Frida version to manage
"""
self.download_dir = Path(settings.DWD_DIR)
self.platform = platform
self.version = version
def clean_up_old_binaries(self):
"""Delete old Frida server binaries."""
if self.platform == 'android':
file_pattern = 'frida-server*'
else:
file_pattern = 'frida_*'
for f in self.download_dir.glob(file_pattern):
if f.is_file() and self.version not in f.name:
try:
f.unlink()
except Exception:
pass
def download_frida_server(self, url, fname, proxies, verify):
"""Download Frida server binary."""
try:
logger.info('Downloading Frida server v%s binary: %s', self.version, fname)
dwd_loc = self.download_dir / fname
with requests.get(
url,
timeout=15,
proxies=proxies,
verify=verify,
stream=True) as r:
r.raise_for_status()
with open(dwd_loc, 'wb') as f:
if fname.endswith('.deb'):
copyfileobj(r.raw, f)
else:
copyfileobj(LZMAFile(r.raw), f)
self.clean_up_old_binaries()
return True
except Exception:
logger.exception(
'[ERROR] Downloading Frida Server v%s Binary', self.version)
# Clean up partial download
try:
dwd_loc.unlink()
except Exception:
pass
return False
def update_frida_server(self, arch):
"""Update/download Frida server for the given architecture."""
if self.platform == 'android':
fserver = f'frida-server-{self.version}-{self.platform}-{arch}'
else:
fserver = f'frida_{self.version}_{self.platform}-{arch}.deb'
frida_bin = self.download_dir / fserver
if frida_bin.is_file():
return True
if not is_internet_available():
return False
try:
proxies, verify = upstream_proxy('https')
except Exception:
logger.exception('[ERROR] Setting upstream proxy')
proxies, verify = None, True
try:
# Get Frida release asset urls
response = requests.get(
f'{settings.FRIDA_SERVER}{self.version}',
timeout=5,
proxies=proxies,
verify=verify,
)
response.raise_for_status()
# Find the correct binary
if self.platform == 'android':
asset = f'{fserver}.xz'
else:
asset = fserver
for item in response.json()['assets']:
if item['name'] == asset:
return self.download_frida_server(
item['browser_download_url'], fserver, proxies, verify,
)
logger.error(
'Frida server v%s binary not found for platform: %s, architecture: %s',
self.version,
self.platform, arch,
)
except Exception:
logger.exception('[ERROR] Fetching Frida Server v%s Release', self.version)
return False
+5 -5
View File
@@ -57,15 +57,15 @@ def safe_paths(tar_meta):
yield fh
def onerror(func, path, exc_info):
_, exc, _ = exc_info
if exc.errno == errno.EACCES: # Permission error
def onexc(func, path, exc_info):
_, exc_value, _ = exc_info
if exc_value.errno == errno.EACCES: # Permission error
try:
os.chmod(path, 0o755)
func(path)
except Exception:
pass
elif exc.errno == errno.ENOTEMPTY: # Directory not empty
elif exc_value.errno == errno.ENOTEMPTY: # Directory not empty
try:
func(path)
except Exception:
@@ -83,7 +83,7 @@ def untar_files(tar_loc, untar_dir):
return False
if untar_dir.exists():
# fix for permission errors
shutil.rmtree(untar_dir, onerror=onerror)
shutil.rmtree(untar_dir, onexc=onexc)
else:
os.makedirs(untar_dir)
with tarfile.open(tar_loc.as_posix(), errorlevel=1) as tar:
@@ -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)
@@ -0,0 +1,448 @@
# -*- coding: utf_8 -*-
"""Connect to iOS device using SSH over USB or WiFi."""
import logging
import shutil
import socket
import subprocess
import threading
import time
import paramiko
from mobsf.DynamicAnalyzer.views.ios.helpers import (
get_ios_model_mapping,
)
logger = logging.getLogger(__name__)
class IOSConnector:
"""Connect to iOS device using SSH over USB or WiFi."""
def __init__(self):
"""Initialize the IOSConnector."""
self.ssh_client = None
self.connection_type = None
self.device_info = {}
self.host = None
self.port = None
self.username = None
self.password = None
self.iproxy_procs = [] # Popen handles for iproxy subprocesses
def _ssh_execute_command(self, command, timeout=30):
"""Execute a command on SSH client."""
if not self.ssh_client:
return None, None, None
# Shared variables for thread communication
result = {'output': '', 'error': '', 'exit_status':
- 1, 'exception': None, 'channel': None}
command_completed = threading.Event()
def execute_command():
"""Execute command in separate thread."""
try:
# Execute command
_, stdout, stderr = self.ssh_client.exec_command(command)
channel = stdout.channel
result['channel'] = channel
# Read output with partial capture
output_parts = []
error_parts = []
# Read stdout in chunks to capture partial output
while True:
try:
chunk = stdout.read(1024).decode('utf-8', errors='ignore')
if not chunk:
break
output_parts.append(chunk)
except Exception:
break
# Read stderr in chunks to capture partial output
while True:
try:
chunk = stderr.read(1024).decode('utf-8', errors='ignore')
if not chunk:
break
error_parts.append(chunk)
except Exception:
break
result['output'] = ''.join(output_parts).strip()
result['error'] = ''.join(error_parts).strip()
# Try to get exit status if available
try:
result['exit_status'] = channel.recv_exit_status()
except Exception:
result['exit_status'] = -1
except Exception as e:
result['exception'] = e
finally:
command_completed.set()
# Start command execution in separate thread
command_thread = threading.Thread(target=execute_command, daemon=True)
command_thread.start()
# Wait for command to complete or timeout
if command_completed.wait(timeout=timeout):
if result['exception']:
logger.error('Command execution failed: %s', str(result['exception']))
return result['output'], result['error'], -1
return result['output'], result['error'], result['exit_status']
else:
if 'oslog' not in command:
logger.warning(
'Command execution timed out after %d seconds: %s',
timeout, command)
# Try to close the channel if it exists
if result['channel']:
try:
result['channel'].close()
except Exception:
pass
# Return any partial output that was captured
return result['output'], result['error'], -1
def connect_usb(self, device_id, username, password, port=2222):
"""Connect to iOS device using USB (via usbmuxd/iproxy)."""
try:
logger.info('Attempting USB connection to iOS device via SSH')
# Check if usbmuxd/iproxy is available
if not self._check_usbmuxd():
raise Exception(
'usbmuxd/iproxy not available. Install libimobiledevice.')
# Find USB connected iOS devices
devices = self.get_usb_devices()
if not devices:
raise Exception('No USB connected iOS devices found')
# List all devices
logger.info('Available USB devices:')
for device in devices:
logger.info(
'Device:\n'
' ID: %s\n'
' Name: %s\n'
' Model: %s\n'
' Version: %s\n'
' Serial: %s',
device.get('id', 'Unknown'),
device.get('name', 'Unknown'),
device.get('model', 'Unknown'),
device.get('version', 'Unknown'),
device.get('serial', 'Unknown'),
)
# Use specified device or first available
if device_id:
device = next((d for d in devices if d['id'] == device_id), None)
if not device:
raise Exception('Device %s not found' % device_id)
else:
device = devices[0]
logger.info(
'Selected device:\n'
' ID: %s\n'
' Name: %s\n'
' Model: %s\n'
' Version: %s\n'
' Serial: %s',
device.get('id', 'Unknown'),
device.get('name', 'Unknown'),
device.get('model', 'Unknown'),
device.get('version', 'Unknown'),
device.get('serial', 'Unknown'),
)
# Setup port forwarding using iproxy
self._setup_usb_port_forward(device['id'], port)
# Prepare device info
device_info = {
'id': device['id'],
'name': device['name'],
'type': 'usb',
'connection': f'localhost:{port}',
}
# Establish SSH connection
self._establish_ssh_connection(
'localhost', port, username, password, 'usb', device_info)
logger.info('Successfully connected to %s via USB SSH', device['name'])
return True
except Exception as e:
logger.error('USB connection failed: %s', str(e))
return False
def connect_wifi(self, ip_address, username, password, port=22):
"""Connect to iOS device using WiFi SSH."""
try:
if port not in range(1, 65536):
raise Exception('Invalid port number')
logger.info('Attempting WiFi SSH connection to %s:%s', ip_address, port)
# Prepare device info
device_info = {
'ip': ip_address,
'port': port,
'type': 'wifi',
'connection': f'{ip_address}:{port}',
}
# Establish SSH connection
self._establish_ssh_connection(
ip_address, port, username, password, 'wifi', device_info)
return True
except Exception as e:
logger.error('WiFi connection failed: %s', str(e))
return False
def disconnect(self):
"""Disconnect from iOS device."""
try:
if self.ssh_client:
self.ssh_client.close()
self.ssh_client = None
# Terminate only the iproxy processes started by this instance
for proc in self.iproxy_procs:
try:
proc.terminate()
except Exception:
pass
self.iproxy_procs = []
logger.info('Disconnected from iOS device')
except Exception as e:
logger.error('Error during disconnect: %s', str(e))
finally:
if hasattr(self, 'ssh_client') and self.ssh_client:
try:
self.ssh_client.close()
except Exception:
pass
self.ssh_client = None
self.connection_type = None
self.device_info = {}
self.host = None
self.port = None
self.username = None
self.password = None
def get_usb_devices(self):
"""Get list of USB connected iOS devices."""
try:
logger.info('Getting iOS devices connected via USB')
if not shutil.which('idevice_id'):
logger.warning(
'libimobiledevice is not installed. '
'Install it with: brew install libimobiledevice')
return []
# Use idevice_id to list devices
result = subprocess.run(['idevice_id', '-l'],
capture_output=True, text=True)
if result.returncode != 0:
return []
devices = []
model_mapping = get_ios_model_mapping()
for device_id in result.stdout.strip().split('\n'):
if device_id:
device_info = {}
# Get device details
output = subprocess.run(['ideviceinfo', '-u', device_id],
capture_output=True, text=True)
if output.returncode != 0:
continue
for line in output.stdout.splitlines():
if ': ' in line:
key, value = line.split(': ', 1)
device_info[key.strip()] = value.strip()
devices.append({
'id': device_id,
'name': device_info.get('DeviceName'),
'model': model_mapping.get(
device_info.get('ProductType'),
device_info.get('ProductType'),
),
'version': device_info.get('ProductVersion'),
'serial': device_info.get('SerialNumber'),
})
return devices
except Exception as e:
logger.error('Failed to get USB devices: %s', str(e))
return []
def _create_ssh_connection(self, host, port, username, password, timeout=10):
"""Create and connect an SSH client."""
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(
hostname=host,
port=port,
username=username,
password=password,
timeout=timeout,
allow_agent=False,
look_for_keys=False,
)
return ssh
def _test_connection_ssh(
self, host, port, username, password, timeout=10):
"""Test SSH connection to iOS device."""
try:
logger.info('Testing SSH connection to %s:%s', host, port)
# Create SSH client
ssh = self._create_ssh_connection(host, port, username, password, timeout)
# Temporarily set self.ssh_client to test client
self.ssh_client = ssh
# Test basic command execution - check user identity
output, _, _ = self._ssh_execute_command('id', timeout)
# Check if we have root access
if 'uid=0(root)' in output:
logger.info('Verified iOS device is Jailbroken')
return True
logger.warning('Not connected as root: %s', output)
ssh.close()
self.ssh_client = None
return False
except Exception as e:
logger.error('SSH connection test failed: %s', str(e))
if self.ssh_client:
try:
self.ssh_client.close()
except Exception:
pass
self.ssh_client = None
return False
def _establish_ssh_connection(
self, host, port, username, password, connection_type, device_info):
"""Establish SSH connection and set up instance variables."""
# Test SSH connection first
if not self._test_connection_ssh(host, port, username, password):
raise Exception('SSH connection test failed')
# Establish SSH connection
if not self.ssh_client:
self.ssh_client = self._create_ssh_connection(
host, port, username, password)
# Set up instance variables
self.connection_type = connection_type
self.host = host
self.port = port
self.username = username
self.password = password
self.device_info = device_info
logger.info('Successfully established SSH connection to %s:%s', host, port)
return True
def _check_usbmuxd(self):
"""Check if usbmuxd/iproxy is available."""
try:
result = subprocess.run(['which', 'iproxy'],
capture_output=True, text=True)
return result.returncode == 0
except Exception:
return False
def _setup_usb_port_forward(self, device_id, port):
"""Setup port forwarding for USB device."""
try:
# Test if ports are open with separate sockets
def test_port(port_to_test):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
result = sock.connect_ex(('localhost', port_to_test))
sock.close()
return result == 0
except Exception:
return False
# Check if port forwarding already exists
ssh_port_open = test_port(port)
frida_port_open = test_port(37042)
if ssh_port_open and frida_port_open:
logger.info(
'Port forwarding already exists: '
'localhost:%s -> device:22, localhost:37042 -> device:27042',
port,
)
return True
# Port forwarding doesn't exist, so set it up
logger.info('Setting up new port forwarding for device %s', device_id)
# Kill any existing iproxy processes for this device (in case they're stuck)
subprocess.run(['pkill', '-f', f'iproxy.*{device_id}'],
capture_output=True)
time.sleep(1)
# Start iproxy for port forwarding; store handles for clean teardown
cmds = [['iproxy', str(port), '22', '-u', device_id],
['iproxy', '37042', '27042', '-u', device_id]]
for cmd in cmds:
logger.info('Starting iproxy: %s', ' '.join(cmd))
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.iproxy_procs.append(proc)
# Wait for port forwarding to be ready
logger.info('Waiting for port forwarding to be ready...')
time.sleep(2) # Give iproxy time to establish connections
# Test if ports are now open
ssh_port_open = test_port(port)
frida_port_open = test_port(37042)
if not ssh_port_open or not frida_port_open:
for proc in self.iproxy_procs:
try:
proc.terminate()
except Exception:
pass
self.iproxy_procs = []
failed_ports = []
if not ssh_port_open:
failed_ports.append(f'SSH port {port}')
if not frida_port_open:
failed_ports.append('Frida port 37042')
raise Exception(
f"Port forwarding failed for: {', '.join(failed_ports)}")
logger.info(
'Port forwarding established: '
'localhost:%s -> device:22, localhost:37042 -> device:27042',
port,
)
return True
except Exception as e:
logger.error('Failed to setup USB port forwarding: %s', str(e))
return False
@@ -0,0 +1,504 @@
# -*- coding: utf_8 -*-
"""iOS device operations using SSH connection."""
import logging
import hashlib
import platform
import subprocess
import base64
import tempfile
import shutil
import io
from pathlib import Path
import time
import plistlib
from django.conf import settings
logger = logging.getLogger(__name__)
class IOSDevice:
"""Handle iOS device operations using SSH connection."""
def __init__(self, connector):
"""Initialize with an IOSConnector instance."""
self.connector = connector
def execute_command(self, command, timeout=30):
"""Execute a command on SSH client."""
if not self.connector.ssh_client:
return None, None, None
return self.connector._ssh_execute_command(command, timeout)
def ps(self):
"""Get the list of running processes."""
if not self.connector.ssh_client:
return None
output, error, exit_code = self.execute_command('ps aux')
if exit_code != 0:
logger.error('Failed to get list of running processes: %s', error)
return None
# Parse ps aux output format
processes = []
lines = output.split('\n')
# Skip header line
for line in lines[1:]:
if line.strip():
try:
# ps aux format:
# USER PID %CPU %MEM VSZ RSS TT STAT START TIME COMMAND
parts = line.split()
if len(parts) >= 11:
pid = parts[1]
command = ' '.join(parts[10:])
# Only include processes with .app in the path
if '.app/' in command:
# Extract app name from .app path
app_parts = command.split('.app/')
if app_parts:
app_path = app_parts[0]
app_name = app_path.split('/')[-1]
processes.append({
'pid': pid,
'process_name': app_name,
})
except Exception as e:
logger.warning(
'Failed to parse process line: %s - %s', line, str(e))
continue
return processes
def get_cpu_architecture(self):
"""Get the device CPU architecture."""
if not self.connector.ssh_client:
return None
try:
err = 'Failed to get device CPU architecture'
output, error, exit_code = self.execute_command('arch')
if exit_code != 0:
logger.error('%s: %s', err, error)
return None
arch = output.strip()
arch_mapping = {
'arm64': 'arm64', # iPhone 5s and later (A7+)
'arm64e': 'arm64', # iPhone XS and later (A12+)
'armv7': 'arm', # iPhone 4, 4S, 5, 5c
'armv7s': 'arm', # iPhone 5, 5c (optimized for Swift)
}
return arch_mapping.get(arch)
except Exception:
logger.exception(err)
return None
def get_platform_architecture(self):
"""Get the device platform architecture."""
if not self.connector.ssh_client:
return None
try:
err = 'Failed to get device platform architecture'
output, error, exit_code = self.execute_command('dpkg --print-architecture')
if exit_code != 0:
logger.error('%s: %s', err, error)
return None
arch = output.strip()
if 'iphoneos-arm64' in arch:
return 'arm64'
return 'arm'
except Exception:
logger.exception(err)
return None
def upload_file(self, local_path, remote_path):
"""Upload a file to the iOS device."""
if not self.connector.ssh_client:
return False
try:
with self.connector.ssh_client.open_sftp() as sftp:
sftp.put(local_path, remote_path)
logger.info('Uploaded %s to %s', local_path, remote_path)
return True
except Exception:
logger.exception('Failed to upload file')
return False
def upload_file_object(self, fobject, filename, path='/tmp/'):
"""Upload a file object to the iOS device."""
if not self.connector.ssh_client:
return False
try:
filename = Path(filename.replace('..', '')).name
path = Path(path.replace('..', ''))
remote_path = path / filename
with self.connector.ssh_client.open_sftp() as sftp:
sftp.putfo(fobject, str(remote_path))
logger.info('Uploaded %s to %s', filename, str(remote_path))
return True
except Exception:
logger.exception('Failed to upload file')
return False
def download_file(self, remote_path, local_path):
"""Download a file from the iOS device."""
if not self.connector.ssh_client:
return False
try:
with self.connector.ssh_client.open_sftp() as sftp:
sftp.get(remote_path, local_path)
logger.info('Downloaded %s to %s', remote_path, local_path)
return True
except FileNotFoundError:
logger.error('File not found: %s', remote_path)
return False
except Exception:
logger.exception('Failed to download file')
return False
def download_file_object(self, remote_path):
"""Return a file object from the iOS device."""
if not self.connector.ssh_client:
return False
try:
with self.connector.ssh_client.open_sftp() as sftp:
with io.BytesIO() as buffer:
sftp.getfo(remote_path, buffer)
buffer.seek(0)
return buffer.read()
except FileNotFoundError:
logger.error('File not found: %s', remote_path)
return False
except Exception:
logger.exception('Failed to download file')
return False
def write_file(self, remote_path, content):
"""Write a file to the iOS device."""
if not self.connector.ssh_client:
return False
try:
with self.connector.ssh_client.open_sftp() as sftp:
with io.BytesIO(content.encode('utf-8')) as buffer:
sftp.putfo(buffer, remote_path)
return True
except Exception:
logger.exception('Failed to write file')
return False
def read_file(self, remote_path):
"""Read a text file from the iOS device."""
if not self.connector.ssh_client:
return None
try:
with self.connector.ssh_client.open_sftp() as sftp:
with io.BytesIO() as buffer:
sftp.getfo(remote_path, buffer)
buffer.seek(0)
return buffer.read().decode('utf-8')
except Exception:
pass
return None
def read_binary_file(self, remote_path):
"""Read a binary file from the iOS device."""
if not self.connector.ssh_client:
return None
try:
with self.connector.ssh_client.open_sftp() as sftp:
with io.BytesIO() as buffer:
sftp.getfo(remote_path, buffer)
buffer.seek(0)
return buffer.read()
except Exception:
logger.exception('Failed to read binary file')
return None
def install_deb(self, remote_path):
"""Install a deb file on the iOS device."""
if not self.connector.ssh_client:
return False
try:
_, error, exit_code = self.execute_command(f'dpkg -i {remote_path}')
if exit_code != 0:
logger.error('Install failed in iOS device: %s', error)
return False
logger.info('Successfully installed deb file: %s', remote_path)
return True
except Exception:
logger.exception('Failed to install deb file')
return False
def install_apt_package(self, package_name):
"""Install an apt package on the iOS device."""
if not self.connector.ssh_client:
return False
try:
_, error, exit_code = self.execute_command(
f'apt-get install -y {package_name}')
if exit_code != 0:
logger.error('Install failed in iOS device: %s', error)
return False
logger.info('Successfully installed apt package: %s', package_name)
return True
except Exception:
logger.exception('Failed to install apt package')
return False
def _appsync_install(self):
"""Check and install AppSync Unified.
Returns:
False: Already installed - caller may proceed with IPA install.
True: Just installed and userspace reboot is in progress.
Caller should abort and retry after the reboot settles.
Raises:
Exception: AppSync could not be installed by either method.
"""
check_install = (
r"apt list --installed | grep -E 'ai\.akemi\.app(inst|syncunified)'"
)
out, _, _ = self.execute_command(check_install)
if 'appsyncunified' in out or 'appinst' in out:
return False # Already installed, proceed
# AppSync is not installed - attempt installation
logger.info('AppSync Unified is not installed. Attempting to install...')
# Method 1: Install from bundled deb file
tools_dir = Path(settings.TOOLS_DIR) / 'ios' / 'appsync'
if 'arm64' in self.get_platform_architecture():
arm64 = ('ai.akemi.appsyncunified_116.0_iphoneos-arm64'
'.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb')
deb_file = tools_dir / arm64
else:
arm = ('ai.akemi.appsyncunified_116.0_iphoneos-arm'
'.akemi-git-235aca6cddfbdc9fa87fcb5b2aec2df37ed6d65a.deb')
deb_file = tools_dir / arm
if not deb_file.exists():
raise Exception('AppSync Unified deb file does not exist: %s' % deb_file)
remote_path = '/tmp/appsync.deb'
self.upload_file(deb_file, remote_path)
self.install_apt_package('mobilesubstrate')
if self.install_deb(remote_path):
logger.info(
'AppSync Unified installed from bundled deb. '
'Rebooting userspace...')
self.execute_command('launchctl reboot userspace')
time.sleep(15)
return True
# Method 2: Install from cydia.akemi.ai
logger.info('Attempting to install AppSync Unified from cydia.akemi.ai')
src_file = '/etc/apt/sources.list.d/cydia.list'
src = 'deb https://cydia.akemi.ai/ ./'
self.execute_command(
f'grep -qxF \'{src}\' {src_file} || echo \'{src}\' >> {src_file}')
self.execute_command('apt update')
_, error, exit_code = self.execute_command(
'apt install -y --allow-unauthenticated ai.akemi.appinst')
if exit_code != 0:
raise Exception(
'Failed to install AppSync Unified via apt: %s' % error)
logger.info(
'AppSync Unified installed via apt. Rebooting userspace...')
self.execute_command('launchctl reboot userspace')
time.sleep(15)
return True
def install_ipa(self, checksum):
"""Install an IPA file on the iOS device."""
if not self.connector.ssh_client:
return False
# Check if AppSync Unified is installed
if self._appsync_install():
logger.info(
'AppSync Unified was just installed and userspace is '
'rebooting. Please try again in a moment.')
return False
ipa_path = Path(settings.UPLD_DIR) / checksum / f'{checksum}.ipa'
if not ipa_path.exists():
logger.error('IPA file does not exist: %s', ipa_path)
return False
if not self.upload_file(ipa_path, f'/tmp/{checksum}.ipa'):
logger.error('Failed to upload IPA file')
return False
out, error, exit_code = self.execute_command(f'appinst /tmp/{checksum}.ipa')
self.execute_command(f'rm -f /tmp/{checksum}.ipa')
if 'Successfully installed' in out:
logger.info('Successfully installed IPA')
return True
if exit_code != 0:
logger.error('Failed to install IPA: %s', error)
else:
logger.error('Failed to install IPA: %s', out)
return False
def list_applications(self):
"""List installed applications."""
if not self.connector.ssh_client:
return []
logger.info('Listing applications on iOS device')
def classify(path):
return 'System' if path.startswith(
'/Applications/') else (
'User' if '/var/containers/Bundle/Application' in path else 'Other')
def checksum(path):
return hashlib.md5(path.encode('utf-8')).hexdigest()
bundle_ids = []
sftp_client = None
try:
sftp_client = self.connector.ssh_client.open_sftp()
output, _, exit_code = self.execute_command('uicache -l')
if exit_code == 0:
for line in output.split('\n'):
if line.strip():
components = line.split(' : ')
bundle_id = components[0].strip()
app_path = components[1].strip()
app_name, app_icon = self.get_app_name(app_path, sftp_client)
bundle_ids.append({
'app_name': app_name,
'app_icon': self.get_app_icon(
app_path, app_icon, sftp_client),
'bundle_id': bundle_id,
'app_path': app_path,
'app_type': classify(app_path),
})
sftp_client.close()
return bundle_ids
else:
logger.error('Failed to get app details')
return []
except Exception:
logger.exception('Failed to get app details')
return []
finally:
if sftp_client:
sftp_client.close()
def get_app_name(self, app_path, sftp):
"""Get name of an app."""
if not self.connector.ssh_client:
return None
try:
plist_path = Path(app_path) / 'Info.plist'
with io.BytesIO() as buffer:
sftp.getfo(str(plist_path), buffer)
buffer.seek(0)
plist = plistlib.load(buffer)
app_name = (
plist.get('CFBundleDisplayName')
or plist.get('CFBundleName')
or plist.get('CFBundleExecutable')
or 'Unknown App'
)
app_icon = None
# First try modern nested structure
try:
icons = (plist['CFBundleIcons']['CFBundlePrimaryIcon']
['CFBundleIconFiles'])
if isinstance(icons, list) and icons:
app_icon = icons[-1] # Return the largest/resolution version
except (KeyError, TypeError):
pass
if not app_icon:
# Fallback to legacy key
app_icon = plist.get('CFBundleIconFile', None)
return app_name, app_icon
except Exception:
logger.exception('Failed to get app name')
return None, None
def _crush_png(self, icon_path):
"""Crush a png file."""
try:
tools_dir = Path(settings.BASE_DIR) / 'StaticAnalyzer' / 'tools' / 'ios'
arch = platform.machine()
system = platform.system()
# Uncrush PNG. CgBI -> PNG
# https://iphonedevwiki.net/index.php/CgBI_file_format
if system == 'Darwin':
args = ['xcrun', '-sdk', 'iphoneos', 'pngcrush', '-q',
'-revert-iphone-optimizations',
icon_path, icon_path + '.fixed']
try:
out = subprocess.run(args, capture_output=True)
if b'libpng error:' in out.stdout:
# PNG looks normal
raise ValueError('PNG is not CgBI')
shutil.move(icon_path + '.fixed', icon_path)
except Exception:
pass
else:
# Windows/Linux
cgbipng_bin = None
if system == 'Windows' and arch in ('AMD64', 'x86'):
cgbipng_bin = 'CgbiPngFix.exe'
elif system == 'Linux' and arch == 'x86_64':
cgbipng_bin = 'CgbiPngFix_amd64'
elif system == 'Linux' and arch == 'aarch64':
cgbipng_bin = 'CgbiPngFix_arm64'
if cgbipng_bin:
cbin = tools_dir / 'CgbiPngFix' / cgbipng_bin
args = [cbin.as_posix(), '-i',
icon_path, '-o', icon_path + '.fixed']
try:
out = subprocess.run(args, capture_output=True)
shutil.move(icon_path + '.fixed', icon_path)
except Exception:
# Fails or PNG is not crushed
pass
else:
logger.warning('CgbiPngFix not available for %s %s', system, arch)
except Exception:
logger.exception('Failed to crush png')
return None
def get_app_icon(self, app_path, app_icon, sftp):
"""Get app icon."""
if not self.connector.ssh_client:
return None
temp_path = None
try:
if app_icon:
# Use the provided icon name from Info.plist
search_pattern = f'{app_icon}*.png'
else:
# Guess icon path
search_pattern = 'AppIcon*.png'
output, _, exit_code = self.execute_command(
f'find "{app_path}" -iname "{search_pattern}"',
)
if exit_code == 0:
if '.png' not in output:
return None
for line in output.split('\n'):
if line.strip():
icon_path = Path(line.strip())
with tempfile.NamedTemporaryFile(
suffix='.png', delete=False) as temp_file:
temp_path = Path(temp_file.name)
sftp.get(str(icon_path), str(temp_path))
self._crush_png(str(temp_path))
return base64.b64encode(
temp_path.read_bytes()).decode('utf-8')
except Exception:
logger.exception('Failed to get app icon')
finally:
if temp_path and temp_path.exists():
temp_path.unlink()
return None
@@ -0,0 +1,508 @@
# -*- coding: utf_8 -*-
"""iOS Dynamic Analysis."""
import logging
import os
import time
from threading import Event, Thread
from pathlib import Path
from django.conf import settings
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from mobsf.MobSF.utils import (
IOS_DEVICE_ID_REGEX,
SSH_DEVICE_ID_REGEX,
get_md5,
is_md5,
parse_host_port,
print_n_send_error_response,
strict_package_check,
)
from mobsf.DynamicAnalyzer.views.common.shared import (
invalid_params,
is_attack_pattern,
send_response,
)
from mobsf.DynamicAnalyzer.forms import UploadFileForm
from mobsf.DynamicAnalyzer.views.ios.helpers import (
configure_proxy,
get_local_ipa_list,
)
from mobsf.DynamicAnalyzer.views.ios.device.device import IOSDevice
from mobsf.DynamicAnalyzer.views.ios.device.connect import IOSConnector
from mobsf.DynamicAnalyzer.views.ios.device.environment import IOSEnvironment
from mobsf.DynamicAnalyzer.views.ios.device.frida import FridaIOSDevice
from mobsf.MobSF.views.authentication import (
login_required,
)
from mobsf.MobSF.views.authorization import (
Permissions,
permission_required,
)
logger = logging.getLogger(__name__)
OK = 'ok'
FAILED = 'failed'
@login_required
@permission_required(Permissions.SCAN)
def dynamic_analysis_device(request, api=False):
"""The iOS Device Dynamic Analysis Entry point."""
try:
if api:
device_id = request.POST.get('device_id', '')
else:
device_id = request.GET.get('device_id', '')
scan_apps = get_local_ipa_list()
connector = IOSConnector()
wifi_devices = get_ios_devices_over_wifi()
if os.getenv('MOBSF_PLATFORM') == 'docker':
devices = []
else:
devices = connector.get_usb_devices()
context = {
'selected_device': device_id,
'apps': scan_apps,
'usb_devices': devices,
'wifi_devices': wifi_devices,
'title': 'MobSF Dynamic Analysis Jailbroken Device',
'version': settings.MOBSF_VER,
}
if api:
return context
template = 'dynamic_analysis/ios/device/dynamic_analysis.html'
return render(request, template, context)
except Exception as exp:
logger.exception('iOS Jailbroken Device Dynamic Analysis')
return print_n_send_error_response(request, exp, api)
@login_required
@permission_required(Permissions.SCAN)
def dynamic_analyzer_device(request, api=False):
"""Dynamic Analyzer for Jailbroken iOS devices."""
try:
if api:
bundleid = request.POST.get('bundle_id')
device_id = request.POST.get('device_id')
form = None
else:
bundleid = request.GET.get('bundle_id')
device_id = request.GET.get('device_id')
form = UploadFileForm()
if not bundleid or not strict_package_check(bundleid):
return print_n_send_error_response(
request,
'Invalid iOS Bundle id',
api)
checksum = get_md5(bundleid.encode('utf-8'))
ios_device, error = validate_and_connect_device(device_id)
if error:
return print_n_send_error_response(
request,
error,
api)
app_dir = Path(settings.UPLD_DIR) / checksum
if not app_dir.exists():
app_dir.mkdir()
configure_proxy(request, bundleid, None)
env = IOSEnvironment(ios_device)
env.setup_or_start_frida()
context = {
'checksum': checksum,
'device_id': device_id,
'bundle_id': bundleid,
'version': settings.MOBSF_VER,
'form': form,
'title': 'iOS Device Dynamic Analyzer'}
template = 'dynamic_analysis/ios/device/dynamic_analyzer.html'
if api:
return context
return render(request, template, context)
except Exception:
logger.exception('iOS Device Dynamic Analyzer')
return print_n_send_error_response(
request,
'iOS Device Dynamic Analysis Failed.',
api)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def ios_instrument_device(request, api=False):
"""Instrument app with frida."""
data = {
'status': FAILED,
'message': ''}
try:
action = request.POST.get('frida_action', 'spawn')
pid = request.POST.get('pid')
new_bundle_id = request.POST.get('new_bundle_id')
device_id = request.POST['device_id']
bundle_id = request.POST['bundle_id']
md5_hash = get_md5(bundle_id.encode('utf-8'))
default_hooks = request.POST['default_hooks']
dump_hooks = request.POST['dump_hooks']
auxiliary_hooks = request.POST['auxiliary_hooks']
code = request.POST['frida_code']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
if not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
if new_bundle_id and not strict_package_check(new_bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
# Fill extras
extras = {}
screenshot_action = request.POST.get('screenshot_action')
if screenshot_action:
extras['screenshot_action'] = screenshot_action.strip()
class_name = request.POST.get('class_name')
if class_name:
extras['class_name'] = class_name.strip()
class_search = request.POST.get('class_search')
if class_search:
extras['class_search'] = class_search.strip()
method_search = request.POST.get('method_search')
if method_search:
extras['method_search'] = method_search.strip()
cls_trace = request.POST.get('class_trace')
if cls_trace:
extras['class_trace'] = cls_trace.strip()
if (is_attack_pattern(default_hooks)
or is_attack_pattern(dump_hooks)
or is_attack_pattern(auxiliary_hooks)
or not is_md5(md5_hash)):
return invalid_params(api)
frida_obj = FridaIOSDevice(
ios_device,
None,
md5_hash,
bundle_id,
default_hooks.split(','),
dump_hooks.split(','),
auxiliary_hooks.split(','),
extras,
code)
if action == 'run':
frida_obj.run_app()
if action == 'spawn':
logger.info('Starting Instrumentation')
frida_obj.spawn()
elif action == 'ps':
logger.info('Enumerating running applications')
data['message'] = frida_obj.ps()
elif action == 'get':
# Get injected Frida script.
data['message'] = frida_obj.get_script(nolog=True)
elif action == 'capture':
# Capture screenshot
screenshot_event = Event()
frida_obj.screenshot_event = screenshot_event
Thread(
target=download_screenshot,
args=(ios_device, md5_hash, screenshot_event),
daemon=True).start()
if action in ('spawn', 'session', 'capture'):
if pid and pid.isdigit():
# Attach to a different pid/bundle id
args = (int(pid), new_bundle_id)
logger.info('Attaching to %s [PID: %s]', new_bundle_id, pid)
else:
# Injecting to existing session/spawn
if action == 'session':
logger.info('Injecting to existing frida session')
args = (None, None)
Thread(target=frida_obj.session, args=args, daemon=True).start()
data['status'] = OK
except Exception as exp:
logger.exception('Frida Instrumentation failed')
data['message'] = str(exp)
return send_response(data, api)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def ssh_execute_device(request, api=False):
"""Execute commands in iOS device over SSH."""
data = {
'status': FAILED,
'message': 'Failed to execute command'}
try:
device_id = request.POST['device_id']
cmd = request.POST['cmd']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
out, err, _ = ios_device.execute_command(cmd)
message = f'{out}\n{err}'
data['status'] = OK
data['message'] = message
except Exception as exp:
data['message'] = str(exp)
logger.exception('Executing Commands in iOS device')
return send_response(data, api)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def upload_file_device(request, api=False):
"""Upload file to device."""
err_msg = 'Failed to upload file'
data = {
'status': FAILED,
'message': err_msg,
}
try:
device_id = request.POST['device_id']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
fobject = request.FILES['file']
upload = ios_device.upload_file_object(
fobject, fobject.name)
if upload:
data['status'] = OK
data['message'] = 'Successfully uploaded file'
else:
data['message'] = 'Failed to upload file'
except Exception as exp:
logger.exception(err_msg)
data['message'] = str(exp)
return send_response(data, api)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST', 'GET'])
def system_logs_device(request, api=False):
"""Show system logs."""
data = {
'status': FAILED,
'message': 'Failed to get system logs',
}
try:
pid = request.GET.get('pid')
if request.method == 'POST':
device_id = request.POST['device_id']
if pid and not pid.isdigit():
data['message'] = 'Invalid PID'
return send_response(data, api)
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
cmd = 'oslog'
if pid:
cmd = f'oslog -p {pid}'
out, err, _ = ios_device.execute_command(cmd, timeout=9)
message = f'{out}\n{err}'
data['status'] = OK
data['message'] = message
return send_response(data, api)
# GET request
logger.info('Getting logs')
device_id = request.GET['device_id']
if pid and not pid.isdigit():
data['message'] = 'Invalid PID'
return send_response(data, api)
usb = IOS_DEVICE_ID_REGEX.match(device_id)
wifi = SSH_DEVICE_ID_REGEX.match(device_id)
if not (usb or wifi):
data['message'] = 'Invalid device id or SSH connection string format'
return send_response(data, api)
template = 'dynamic_analysis/ios/device/system_logs.html'
return render(request,
template,
{'device_id': device_id,
'pid': pid,
'version': settings.MOBSF_VER,
'title': 'Live logs'})
except Exception as exp:
logger.exception('Getting logs')
data['message'] = str(exp)
return send_response(data, api)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def ps_device(request, api=False):
"""Get list of running processes on iOS device."""
data = {
'status': FAILED,
'message': 'Failed to get process list',
}
try:
device_id = request.POST.get('device_id', '')
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
processes = ios_device.ps()
if processes:
data['status'] = OK
data['message'] = processes
else:
data['message'] = 'No processes found'
except Exception as exp:
logger.exception('Getting process list failed')
data['message'] = str(exp)
return send_response(data, api)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def get_ios_device(request, api=False):
"""Get details of selected iOS device."""
data = {
'status': 'failed',
'message': 'Failed to connect to iOS device',
}
try:
device_id = request.POST.get('device_id', '')
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
data['apps'] = ios_device.list_applications()
data['status'] = OK
data['message'] = 'Successfully connected to iOS device'
except Exception:
logger.exception('Failed to connect to iOS device')
return send_response(data, api)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def install_ipa_device(request, api=False):
"""Install an IPA file on the device."""
data = {
'status': 'failed',
'message': 'Failed to install IPA on device',
}
try:
device_id = request.POST.get('device_id', '')
# Checksum from IPA SAST Upload location
checksum = request.POST.get('checksum', '')
if not is_md5(checksum):
data['message'] = 'Invalid checksum format'
return send_response(data, api)
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
if not ios_device.install_ipa(checksum):
data['message'] = 'Failed to install IPA'
return send_response(data, api)
data['status'] = OK
data['message'] = 'Successfully installed IPA'
except Exception:
logger.exception('Failed to install IPA on iOS device')
return send_response(data, api)
# Helpers
def get_ios_devices_over_wifi():
"""Get available iOS devices over WiFi."""
devices = []
connect_strings = None
connect_strings_settings = getattr(settings, 'IOS_ANALYZER_IDENTIFIERS', '')
if os.getenv('MOBSF_IOS_ANALYZER_IDENTIFIERS'):
connect_strings = os.getenv('MOBSF_IOS_ANALYZER_IDENTIFIERS')
elif connect_strings_settings:
connect_strings = connect_strings_settings
try:
if not connect_strings:
return devices
if ',' in connect_strings:
# Multiple devices
for device in connect_strings.split(','):
devices.append(parse_host_port(device.strip()))
else:
# Single device
devices.append(parse_host_port(connect_strings.strip()))
except ValueError:
pass
return devices
def get_ios_device_wifi_connect_string(ssh_string):
"""Get iOS device connect string."""
available_devices = get_ios_devices_over_wifi()
if not ssh_string:
return None, None
ip, port = parse_host_port(ssh_string)
if (ip, port) in available_devices:
return ip, port
return None, None
def download_screenshot(ios_device, checksum, screenshot_event=None):
"""Download screenshot from device."""
try:
# Wait for the Frida script to signal screenshot completion
# rather than using a fixed sleep, with a 30s timeout as fallback.
if screenshot_event:
screenshot_event.wait(timeout=30)
else:
time.sleep(5)
timestamp = time.strftime('%Y%m%d%H%M%S')
path = Path(settings.DWD_DIR) / f'{checksum}-sshot-{timestamp}.png'
ios_device.download_file('/tmp/screenshot.png', path)
except Exception as exp:
logger.error('Failed to download screenshot from iOS device')
logger.error(exp)
def validate_and_connect_device(device_id):
"""Validate device_id and establish connection to iOS device."""
usb = IOS_DEVICE_ID_REGEX.match(device_id)
wifi = SSH_DEVICE_ID_REGEX.match(device_id)
if not (usb or wifi):
return None, 'Invalid device id or SSH connection string format'
ssh_user = getattr(settings, 'IOS_SSH_USER', 'root')
ssh_password = getattr(settings, 'IOS_SSH_PASSWORD', 'alpine')
connector = IOSConnector()
if usb:
connected = connector.connect_usb(
device_id, username=ssh_user, password=ssh_password)
if not connected:
return None, 'Failed to connect to iOS device over USB'
else:
ip, port = get_ios_device_wifi_connect_string(device_id)
connected = connector.connect_wifi(
ip, ssh_user, ssh_password, port=port)
if not connected:
return None, 'Failed to connect to iOS device over WiFi'
ios_device = IOSDevice(connector)
return ios_device, None
@@ -0,0 +1,319 @@
# -*- coding: utf_8 -*-
"""iOS Device Environment for Dynamic Analysis."""
import io
import logging
import plistlib
import time
from pathlib import Path
from django.conf import settings
from frida import __version__ as frida_version
from mobsf.DynamicAnalyzer.views.common.frida.server_update import (
FridaServerUpdater,
)
logger = logging.getLogger(__name__)
class IOSEnvironment:
"""iOS Device Environment for Dynamic Analysis."""
def __init__(self, ios_device):
"""Initialize iOS Environment with device operations.
Args:
ios_device: IOSDevice instance
"""
self.ios_device = ios_device
self.connector = ios_device.connector
self.frida_server_path = None
self.frida_process = None
self.frida_plist_path = None
self.platform = None
self.platform_architecture = self.ios_device.get_platform_architecture()
def _find_frida_server_plist(self):
"""Find Frida server plist file on iOS device."""
if self.frida_plist_path:
return self.frida_plist_path
try:
# Common locations where Frida server plist might be installed
plist_locations = [
'/Library/LaunchDaemons/re.frida.server.plist',
'/var/jb/Library/LaunchDaemons/re.frida.server.plist',
'/System/Library/LaunchDaemons/re.frida.server.plist',
]
for plist_path in plist_locations:
output, _, exit_code = self.ios_device.execute_command(
f'ls -la {plist_path}',
)
if exit_code == 0 and 'No such file' not in output:
self.frida_plist_path = plist_path
return plist_path
# If not found in common locations, search for it
logger.info('Searching for Frida server plist files...')
output, _, exit_code = self.ios_device.execute_command(
'find / -name "*frida*server*.plist" 2>/dev/null',
)
if exit_code == 0 and output.strip():
plist_files = output.strip().split('\n')
for plist_file in plist_files:
if plist_file.strip():
self.frida_plist_path = plist_file.strip()
return plist_file.strip()
logger.warning('No Frida server plist file found')
return None
except Exception:
logger.exception('[ERROR] Finding Frida server plist')
return None
def _install_frida_server_on_device(self):
"""Install Frida server on iOS device."""
try:
logger.info('Installing Frida Server v%s on iOS device', frida_version)
# Upload the binary to the device
success = self.ios_device.upload_file(
self.frida_server_path,
'/tmp/fd.deb',
)
if not success:
raise Exception(
'Failed to upload Frida server v%s binary to device', frida_version)
success = self.ios_device.install_deb('/tmp/fd.deb')
if not success:
raise Exception(
'Failed to install Frida server v%s on device', frida_version)
return True
except Exception:
logger.exception(
'[ERROR] Installing Frida server v%s on iOS device', frida_version)
return False
def _modify_frida_plist_for_wifi(self):
"""Modify Frida server plist to enable WiFi access using plistlib."""
try:
self._find_frida_server_plist()
if not self.frida_plist_path:
logger.error('No Frida plist path available')
return False
logger.info('Modifying Frida plist for WiFi access')
# Read as raw bytes — handles both XML and binary plist formats.
# read_file() would fail on binary plists due to UTF-8 decoding.
plist_bytes = self.ios_device.read_binary_file(self.frida_plist_path)
if not plist_bytes:
raise Exception('Failed to read Frida plist')
# Quick check for already-patched XML plist (avoids re-parsing)
if (b'<string>-l</string>' in plist_bytes
and b'<string>0.0.0.0</string>' in plist_bytes):
logger.info('WiFi arguments already present in plist')
return True
# plistlib.loads() handles both XML and binary plist formats
plist = plistlib.loads(plist_bytes)
args = plist.get('ProgramArguments', [])
if '-l' not in args:
args.append('-l')
if '0.0.0.0' not in args:
args.append('0.0.0.0')
plist['ProgramArguments'] = args
logger.info('Added -l 0.0.0.0 to ProgramArguments')
# Serialize back to XML and upload in-memory
updated_bytes = plistlib.dumps(plist, fmt=plistlib.FMT_XML)
plist_name = Path(self.frida_plist_path).name
plist_dir = str(Path(self.frida_plist_path).parent) + '/'
self.ios_device.upload_file_object(
io.BytesIO(updated_bytes), plist_name, plist_dir)
logger.info('Successfully modified Frida plist for WiFi access')
return True
except Exception:
logger.exception('[ERROR] Modifying Frida plist for WiFi access')
return False
def _enable_frida_on_wifi(self):
"""Enable Frida on WiFi."""
try:
logger.info('Enabling Frida Server v%s on 0.0.0.0', frida_version)
self._find_frida_server_plist()
if not self.frida_plist_path:
raise ValueError('Frida server plist not found')
# Modify the plist to add WiFi arguments
if not self._modify_frida_plist_for_wifi():
raise ValueError('Failed to modify plist for WiFi access')
# Reload the modified plist
self.start_frida_server()
logger.info(
'Successfully started Frida Server v%s on (0.0.0.0)', frida_version)
return True
except Exception:
logger.exception(
'[ERROR] Enabling Frida Server v%s on 0.0.0.0', frida_version)
return False
def setup_or_start_frida(self):
"""Setup and start Frida server for iOS device."""
try:
# Check if setup is already done
if self.ios_device.read_file('/tmp/frida_setup_done') == frida_version:
self.start_frida_server()
logger.info(
'Successfully started Frida Server v%s on (0.0.0.0)', frida_version)
return True
# Get iOS platform architecture
if not self.platform_architecture:
raise ValueError('Failed to determine iOS platform architecture')
logger.info('iOS platform architecture identified as %s',
self.platform_architecture)
# Update/download Frida server
platform = 'iphoneos'
frida_bin = (
f'frida_{frida_version}_{platform}-{self.platform_architecture}.deb')
self.frida_server_path = Path(settings.DWD_DIR) / frida_bin
if not self.frida_server_path.is_file():
# Download Frida server binary if not found
updater = FridaServerUpdater(platform, frida_version)
success = updater.update_frida_server(
self.platform_architecture,
)
if not success:
msg = (
'Cannot download frida binary for iOS. You will need'
f' {frida_bin} in {settings.DWD_DIR}'
' for Dynamic Analysis to work')
raise ValueError(msg)
# install frida server on device
self._install_frida_server_on_device()
# Enable and start Frida on WiFi
self._enable_frida_on_wifi()
# Install oslog on device
self.install_oslog()
# Create a file to indicate that frida setup is done
self.ios_device.write_file('/tmp/frida_setup_done', frida_version)
return True
except Exception:
logger.exception('[ERROR] Setting up Frida for iOS')
return False
def start_frida_server(self):
"""Start Frida Server on iOS device using launchctl."""
try:
self.stop_frida_server()
# Load and start the Frida server using launchctl
output, _, exit_code = self.ios_device.execute_command(
f'launchctl load {self.frida_plist_path}',
)
if exit_code != 0:
raise ValueError('Failed to load Frida server plist')
# Wait for the server to start
logger.info('Waiting for Frida server to start...')
# Verify the server is running
output, _, exit_code = self.ios_device.execute_command(
'ps aux | grep frida-server | grep -v grep',
)
if exit_code != 0:
# Attempt to start the server using the binary
_, error, exit_code = self.ios_device.execute_command(
'frida-server -l 0.0.0.0 &',
)
if exit_code != 0:
raise ValueError(
f'Failed to start Frida server using binary: {error}',
)
output, _, exit_code = self.ios_device.execute_command(
'ps aux | grep frida-server | grep -v grep',
)
time.sleep(1)
if exit_code == 0 and output.strip():
logger.info('Frida Server started successfully')
return True
else:
raise Exception('Frida Server failed to start')
except Exception:
logger.exception('[ERROR] Running Frida server on iOS device')
return False
def stop_frida_server(self):
"""Stop Frida server on iOS device using launchctl."""
try:
self._find_frida_server_plist()
if not self.frida_plist_path:
raise Exception('Frida server plist not found')
# Unload the plist using launchctl
output, error, exit_code = self.ios_device.execute_command(
f'launchctl unload {self.frida_plist_path}',
)
if exit_code == 0:
logger.info('Successfully stopped Frida server')
else:
logger.warning('Failed to stop Frida server: %s', error)
# Also kill any remaining Frida server processes as fallback
_, error, exit_code = self.ios_device.execute_command(
'pkill -f frida-server',
)
if exit_code == 0:
logger.info('Killed remaining Frida server processes')
return True
except Exception:
logger.exception('[ERROR] Stopping Frida server')
return False
def install_oslog(self):
"""Install oslog on iOS device."""
try:
logger.info('Installing oslog on iOS device')
if not self.platform_architecture:
raise ValueError('Failed to determine iOS platform architecture')
tools_dir = Path(settings.TOOLS_DIR) / 'ios' / 'oslog'
if self.platform_architecture == 'arm64':
oslog_bin = 'oslog_0.0.4.3_iphoneos-arm64.deb'
else:
oslog_bin = 'oslog_0.0.1-8_iphoneos-arm.deb'
deb_file = tools_dir / oslog_bin
if not deb_file.exists():
raise ValueError('oslog deb file does not exist: %s', deb_file)
remote_path = '/tmp/oslog.deb'
self.ios_device.upload_file(deb_file, remote_path)
if not self.ios_device.install_deb(remote_path):
raise ValueError('Failed to install oslog on device')
return True
except Exception:
logger.exception('[ERROR] Installing oslog on iOS device')
return False
@@ -0,0 +1,179 @@
"""Frida Core Class for iOS Device."""
import logging
import time
import sys
import frida
from mobsf.DynamicAnalyzer.views.ios.frida_core import Frida
from mobsf.DynamicAnalyzer.views.ios.device.environment import IOSEnvironment
logger = logging.getLogger(__name__)
_PID = None
_SPAWN_LOG_MSG = 'Spawned %s with PID %s'
class FridaIOSDevice(Frida):
"""Frida iOS Device for Dynamic Analysis."""
def __init__(
self, ios_device, ssh_string, app_hash, bundle_id,
defaults, dump, auxiliary, extras, code):
"""Initialize."""
super().__init__(
ssh_string, app_hash, bundle_id, defaults, dump, auxiliary, extras, code)
self.connector = ios_device.connector
self.env = IOSEnvironment(ios_device)
self.api = None
# USB path: iproxy maps localhost:37042 -> device:27042
# MobSF starts this automatically via _setup_usb_port_forward().
# Frida connects to localhost:37042.
#
# WiFi / Docker path: Frida connects directly to the device host
# on the default Frida port (27042). No local port mapping is
# created by MobSF. When running in Docker/Colima, run iproxy
# on the macOS host before starting MobSF:
# iproxy -s 0.0.0.0 2222:22 27042:27042 -u <udid>
# Then set MOBSF_IOS_ANALYZER_IDENTIFIERS=host.lima.internal:2222
# (Colima) or host.docker.internal:2222 (Docker Desktop).
if self.connector.connection_type == 'usb':
self.frida_device = frida.get_device_manager().add_remote_device(
f'{self.connector.host}:37042')
else:
self.frida_device = frida.get_device_manager().add_remote_device(
self.connector.host)
def run_app(self):
"""Run the app with frida."""
pid = None
try:
try:
pid = self.frida_device.spawn([self.bundle_id])
self.frida_device.resume(pid)
logger.info(_SPAWN_LOG_MSG, self.bundle_id, pid)
return pid
except frida.NotSupportedError:
logger.error(self.not_supported_text)
logger.info(_SPAWN_LOG_MSG, self.bundle_id, pid)
return pid
except frida.ServerNotRunningError:
self.env.start_frida_server()
time.sleep(1)
pid = self.frida_device.spawn([self.bundle_id])
self.frida_device.resume(pid)
logger.info(_SPAWN_LOG_MSG, self.bundle_id, pid)
return pid
except frida.TimedOutError:
logger.error('Timed out while waiting for device to appear')
except frida.ServerNotRunningError:
logger.error('Frida Server is not running')
except frida.NotSupportedError:
logger.error(self.not_supported_text)
except (frida.ProcessNotFoundError,
frida.ProcessNotRespondingError,
frida.TransportError,
frida.InvalidOperationError):
pass
except Exception:
logger.exception('Failed to run app')
return None
def spawn(self):
"""Connect to Frida Server and spawn the app."""
global _PID
try:
try:
self.clean_up()
_PID = self.frida_device.spawn([self.bundle_id])
except frida.NotSupportedError:
logger.error(self.not_supported_text)
return
except frida.ServerNotRunningError:
self.env.start_frida_server()
_PID = None
if not _PID:
_PID = self.frida_device.spawn([self.bundle_id])
logger.info(_SPAWN_LOG_MSG, self.bundle_id, _PID)
except frida.TimedOutError:
logger.error('Timed out while waiting for device to appear')
except frida.ServerNotRunningError:
logger.error('Frida Server is not running')
except frida.NotSupportedError:
logger.error(self.not_supported_text)
return
except (frida.ProcessNotFoundError,
frida.ProcessNotRespondingError,
frida.TransportError,
frida.InvalidOperationError):
pass
except Exception:
logger.exception('Error Connecting to Frida Server')
def session(self, pid, bundle_id):
"""Use existing session to inject frida scripts."""
global _PID
try:
try:
if pid and bundle_id:
_PID = pid
self.bundle_id = bundle_id
try:
front = self.frida_device.get_frontmost_application()
if front or front.pid != _PID:
# Not the front most app.
# Get the pid of the front most app
logger.warning('Front most app has PID %s', front.pid)
_PID = front.pid
except Exception:
pass
self.frida_device.resume(_PID)
time.sleep(2)
session = self.frida_device.attach(_PID)
except frida.NotSupportedError:
logger.error(self.not_supported_text)
return
except Exception:
logger.warning('Cannot attach to pid, spawning again.')
self.spawn()
self.frida_device.resume(_PID)
time.sleep(2)
session = self.frida_device.attach(_PID)
if session and self.frida_device and _PID:
script = session.create_script(self.get_script())
script.on('message', self.frida_response)
script.load()
self.api = script.exports_sync
self.api_handler(self.api)
sys.stdin.read()
script.unload()
session.detach()
except frida.NotSupportedError:
logger.error(self.not_supported_text)
except (frida.ProcessNotFoundError,
frida.ProcessNotRespondingError,
frida.TransportError,
frida.InvalidOperationError):
pass
except Exception:
logger.exception('Error Connecting to Frida Server')
def ps(self):
"""Get running process pid."""
ps_dict = []
try:
try:
processes = self.frida_device.enumerate_applications(scope='minimal')
except frida.ServerNotRunningError:
self.env.start_frida_server()
processes = self.frida_device.enumerate_applications(scope='minimal')
if self.frida_device and processes:
for process in processes:
if process.pid != 0:
ps_dict.append({
'pid': process.pid,
'name': process.name,
'identifier': process.identifier,
})
except Exception:
logger.exception('Failed to enumerate running applications')
return ps_dict
@@ -0,0 +1,312 @@
# -*- coding: utf_8 -*-
"""Dynamic Analyzer Reporting for iOS devices."""
import logging
import shutil
from pathlib import Path
import json
from django.conf import settings
from django.shortcuts import render
from django.template.defaulttags import register
from django.views.decorators.http import require_http_methods
from django.http import HttpResponse
import mobsf.MalwareAnalyzer.views.Trackers as Trackers
from mobsf.DynamicAnalyzer.views.ios.analysis import (
get_screenshots,
ios_api_analysis,
run_analysis,
)
from mobsf.DynamicAnalyzer.views.ios.device.dynamic_analyzer import (
FAILED,
OK,
validate_and_connect_device,
)
from mobsf.DynamicAnalyzer.tools.webproxy import (
get_http_tools_url,
stop_httptools,
)
from mobsf.MobSF.utils import (
IOS_DEVICE_ID_REGEX,
SSH_DEVICE_ID_REGEX,
base64_decode,
get_md5,
is_md5,
key,
pretty_json,
print_n_send_error_response,
replace,
strict_package_check,
)
from mobsf.MobSF.views.authentication import (
login_required,
)
from mobsf.DynamicAnalyzer.views.common.shared import (
send_response,
)
from mobsf.MobSF.views.authorization import (
Permissions,
permission_required,
)
logger = logging.getLogger(__name__)
register.filter('key', key)
register.filter('replace', replace)
register.filter('pretty_json', pretty_json)
register.filter('base64_decode', base64_decode)
def download_app_data_device(ios_device, checksum):
"""Download App data from device."""
app_dir = Path(settings.UPLD_DIR) / checksum
container_file = app_dir / 'mobsf_app_container_path.txt'
if container_file.exists():
app_container = container_file.read_text(
'utf-8').splitlines()[0].strip()
tarfile = f'/tmp/{checksum}-app-container.tar'
localtar = app_dir / f'{checksum}-app-container.tar'
ios_device.execute_command(f'tar -C {app_container} -cvf {tarfile} .')
ios_device.download_file(tarfile, localtar)
if localtar.exists():
dst = Path(settings.DWD_DIR) / f'{checksum}-app_data.tar'
shutil.copyfile(localtar, dst)
# JSON API
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def download_data_device(request, bundle_id, api=False):
"""Download Application Data from Device."""
logger.info('Downloading application data')
data = {
'status': FAILED,
'message': 'Failed to Download application data'}
try:
device_id = request.POST['device_id']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
if not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
checksum = get_md5(bundle_id.encode('utf-8'))
# App Container download
logger.info('Downloading app container data')
download_app_data_device(ios_device, checksum)
# Stop HTTPS Proxy
stop_httptools(get_http_tools_url(request))
# Move HTTP raw logs to download directory
flows = Path.home() / '.httptools' / 'flows'
webf = flows / f'{bundle_id}.flow.txt'
dwd = Path(settings.DWD_DIR)
dweb = dwd / f'{checksum}-web_traffic.txt'
if webf.exists():
shutil.copyfile(webf, dweb)
data = {
'status': OK,
'message': 'Downloaded application data',
}
except Exception as exp:
logger.exception('Downloading application data')
data['message'] = str(exp)
return send_response(data, api)
@login_required
def view_report_device(request, bundle_id, api=False):
"""Dynamic Analysis Report Generation."""
logger.info('iOS Dynamic Analysis Report Generation')
try:
if api:
device_id = request.POST.get('device_id')
else:
device_id = request.GET.get('device_id')
if device_id:
usb = IOS_DEVICE_ID_REGEX.match(device_id)
wifi = SSH_DEVICE_ID_REGEX.match(device_id)
if not (usb or wifi):
return print_n_send_error_response(
request,
'Invalid device id or SSH connection string format',
api)
if not strict_package_check(bundle_id):
# bundle_id is not validated in REST API.
# Also bundleid is not strictly validated
# in URL path.
return print_n_send_error_response(
request,
'Invalid iOS Bundle id',
api)
checksum = get_md5(bundle_id.encode('utf-8'))
app_dir = Path(settings.UPLD_DIR) / checksum
download_dir = settings.DWD_DIR
tools_dir = settings.TOOLS_DIR
frida_log = app_dir / 'mobsf_frida_out.txt'
data_dir = app_dir / 'DYNAMIC_DeviceData'
if not (frida_log.exists() or data_dir.exists()):
msg = ('Dynamic Analysis report is not available '
'for this app. Perform Dynamic Analysis '
'and generate the report.')
return print_n_send_error_response(request, msg, api)
api_analysis = ios_api_analysis(app_dir)
dump_analysis = run_analysis(app_dir, bundle_id, checksum)
trk = Trackers.Trackers(checksum, app_dir, tools_dir)
trackers = trk.get_trackers_domains_or_deps(
dump_analysis['domains'], None)
screenshots = get_screenshots(checksum, download_dir)
logger.info('Report generation completed')
context = {
'hash': checksum,
'device_id': device_id,
'version': settings.MOBSF_VER,
'title': 'iOS Dynamic Analysis Report',
'bundleid': bundle_id,
'trackers': trackers,
'screenshots': screenshots,
'frida_logs': frida_log.exists(),
}
context.update(api_analysis)
context.update(dump_analysis)
template = 'dynamic_analysis/ios/device/dynamic_report.html'
if api:
return context
return render(request, template, context)
except Exception as exp:
logger.exception('Dynamic Analysis Report Generation')
err = f'Error Generating Dynamic Analysis Report. {str(exp)}'
return print_n_send_error_response(request, err, api)
# JSON API + File Download
@login_required
@permission_required(Permissions.SCAN)
@require_http_methods(['POST'])
def download_file_device(request, api=False):
"""Download file from device."""
err_msg = 'Failed to download file'
data = {
'status': FAILED,
'message': err_msg,
}
try:
remote_path = request.POST['file']
device_id = request.POST['device_id']
ios_device, error = validate_and_connect_device(device_id)
if error:
data['message'] = error
return send_response(data, api)
file_obj = ios_device.download_file_object(remote_path)
if file_obj:
response = HttpResponse(file_obj, content_type='application/octet-stream')
response['Content-Disposition'] = (
f'inline; filename={Path(remote_path).name}')
return response
else:
data['message'] = 'Failed to download file'
except Exception as exp:
logger.exception(err_msg)
data['message'] = str(exp)
return send_response(data, api)
# JSON API
@login_required
def ios_api_monitor(request, api=False):
try:
data = {
'status': FAILED,
'message': 'Failed to get API monitor data',
}
if api:
checksum = request.POST['checksum']
stream = True
else:
checksum = request.GET.get('checksum', '')
stream = request.GET.get('stream', '')
bundle_id = request.GET.get('bundle_id', '')
if bundle_id and not strict_package_check(bundle_id):
data['message'] = 'Invalid iOS Bundle id'
return send_response(data, api)
if not is_md5(checksum):
data['message'] = 'Invalid checksum format'
return send_response(data, api)
if stream:
app_dir = Path(settings.UPLD_DIR) / checksum
apimon_file = app_dir / 'mobsf_dump_file.txt'
data = {}
if not apimon_file.exists():
data['message'] = 'Data does not exist.'
return send_response(data, api)
# Read the file and parse each line as JSON
lines = apimon_file.read_text(encoding='utf-8').splitlines()
# remove all duplicate lines
lines = list(set(lines))
api_data = []
for line in lines:
if line.strip():
try:
# Parse each line as JSON
json_obj = json.loads(line)
# Determine the API type based on the keys in the JSON object
api_type = None
if 'cookies' in json_obj:
api_type = 'Cookies'
elif 'keychain' in json_obj:
api_type = 'Keychain'
elif 'json' in json_obj:
api_type = 'JSON Data'
elif 'network' in json_obj:
api_type = 'Network Request'
elif 'sql' in json_obj:
api_type = 'SQL Query'
elif 'filename' in json_obj:
api_type = 'File Access'
elif 'crypto' in json_obj:
api_type = 'Crypto'
elif 'files' in json_obj:
api_type = 'Files'
elif 'nslog' in json_obj:
api_type = 'Logs'
elif 'credentialstorage' in json_obj:
api_type = 'Credentials'
elif 'nsuserdefaults' in json_obj:
api_type = 'UserDefaults'
elif 'pasteboard' in json_obj:
api_type = 'Pasteboard'
elif 'textinput' in json_obj:
api_type = 'Text Inputs'
elif 'datadir' in json_obj:
api_type = 'Data Directory'
else:
# If no known key, use the first key as API type
api_type = list(json_obj.keys())[
0] if json_obj else 'Unknown'
# Create the data object for DataTable
api_data.append({
'api': api_type,
'data': json_obj,
})
except json.JSONDecodeError:
# Skip invalid JSON lines
continue
data['data'] = api_data
return send_response(data, api)
template = 'dynamic_analysis/ios/api_monitor.html'
return render(request,
template,
{'checksum': checksum,
'bundle_id': bundle_id,
'version': settings.MOBSF_VER,
'title': 'API Monitor'})
except Exception:
logger.exception('API monitor streaming')
err = 'Error in API monitor streaming'
return print_n_send_error_response(request, err, api)
@@ -3,7 +3,6 @@
import logging
import os
from pathlib import Path
from threading import Thread
from django.conf import settings
from django.shortcuts import render
@@ -12,26 +11,20 @@ from mobsf.MobSF.utils import (
common_check,
get_md5,
print_n_send_error_response,
python_dict,
strict_package_check,
)
from mobsf.StaticAnalyzer.models import StaticAnalyzerIOS
from mobsf.DynamicAnalyzer.forms import UploadFileForm
from mobsf.DynamicAnalyzer.views.ios.helpers import (
configure_proxy,
get_local_ipa_list,
)
from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import (
generate_keypair_if_not_exists,
)
from mobsf.DynamicAnalyzer.tools.webproxy import (
get_http_tools_url,
start_proxy,
stop_httptools,
)
from mobsf.DynamicAnalyzer.views.ios.corellium_apis import (
CorelliumAPI,
CorelliumInstanceAPI,
)
from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import (
ssh_jumphost_reverse_port_forward,
)
from mobsf.MobSF.views.authentication import (
login_required,
)
@@ -48,30 +41,7 @@ logger = logging.getLogger(__name__)
def dynamic_analysis(request, api=False):
"""The iOS Dynamic Analysis Entry point."""
try:
scan_apps = []
ipas = StaticAnalyzerIOS.objects.filter(
FILE_NAME__endswith='.ipa')
for ipa in reversed(ipas):
bundle_hash = get_md5(ipa.BUNDLE_ID.encode('utf-8'))
frida_dump = Path(
settings.UPLD_DIR) / bundle_hash / 'mobsf_dump_file.txt'
macho = python_dict(ipa.MACHO_ANALYSIS)
encrypted = False
if (macho
and macho.get('encrypted')
and macho.get('encrypted').get('is_encrypted')):
encrypted = macho['encrypted']['is_encrypted']
temp_dict = {
'MD5': ipa.MD5,
'APP_NAME': ipa.APP_NAME,
'APP_VERSION': ipa.APP_VERSION,
'FILE_NAME': ipa.FILE_NAME,
'BUNDLE_ID': ipa.BUNDLE_ID,
'BUNDLE_HASH': bundle_hash,
'ENCRYPTED': encrypted,
'DYNAMIC_REPORT_EXISTS': frida_dump.exists(),
}
scan_apps.append(temp_dict)
scan_apps = get_local_ipa_list()
# Corellium
instances = []
project_id = None
@@ -103,17 +73,17 @@ def dynamic_analyzer(request, api=False):
try:
if api:
bundleid = request.POST.get('bundle_id')
instance_id = request.POST.get('instance_id')
form = None
else:
bundleid = request.GET.get('bundleid')
bundleid = request.GET.get('bundle_id')
instance_id = request.GET.get('instance_id')
form = UploadFileForm()
if not bundleid or not strict_package_check(bundleid):
return print_n_send_error_response(
request,
'Invalid iOS Bundle id',
api)
if api:
instance_id = request.POST.get('instance_id')
else:
instance_id = request.GET.get('instance_id')
failed = common_check(instance_id)
if failed:
return print_n_send_error_response(
@@ -126,12 +96,8 @@ def dynamic_analyzer(request, api=False):
app_dir.mkdir()
ci = CorelliumInstanceAPI(instance_id)
configure_proxy(request, bundleid, ci)
if api:
form = None
else:
form = UploadFileForm()
context = {
'hash': bundle_hash,
'checksum': bundle_hash,
'instance_id': instance_id,
'bundle_id': bundleid,
'version': settings.MOBSF_VER,
@@ -185,16 +151,3 @@ def setup_ssh_keys(c):
logger.error('Failed to add SSH Key to Corellium project')
return
logger.info('Added SSH Key to Corellium project')
def configure_proxy(request, project, ci):
"""Configure HTTPS Proxy."""
proxy_port = settings.PROXY_PORT
logger.info('Starting HTTPS Proxy on %s', proxy_port)
stop_httptools(get_http_tools_url(request))
start_proxy(proxy_port, project)
# Remote Port forward for HTTPS Proxy
logger.info('Starting Remote Port Forward over SSH')
Thread(target=ssh_jumphost_reverse_port_forward,
args=(ci.get_ssh_connection_string(),),
daemon=True).start()
+24 -10
View File
@@ -1,13 +1,19 @@
import logging
from threading import Thread
from pathlib import Path
import sys
import time
from django.conf import settings
from pathlib import Path
from threading import Thread
import frida
from django.conf import settings
from mobsf.DynamicAnalyzer.views.common.frida import (
get_bridge_loader,
)
from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import (
ssh_jumphost_port_forward,
)
from mobsf.DynamicAnalyzer.views.ios.frida_auxiliary_scripts import (
class_pattern,
class_trace,
@@ -18,12 +24,9 @@ from mobsf.DynamicAnalyzer.views.ios.frida_auxiliary_scripts import (
string_capture,
string_compare,
)
from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import (
ssh_jumphost_port_forward,
)
from mobsf.DynamicAnalyzer.views.common.frida import (
get_bridge_loader,
)
SCREENSHOT_CAPTURED_MSG = '✅ Screenshot Captured'
logger = logging.getLogger(__name__)
_PID = None
@@ -61,6 +64,7 @@ class Frida:
'Are you able to run the app on '
'this device?')
self.bridge_loader = get_bridge_loader()
self.screenshot_event = None
def get_scripts(self, script_type, selected_scripts):
"""Get Frida Scripts."""
@@ -157,6 +161,9 @@ class Frida:
else:
logger.debug('[Frida] %s', msg)
self.write_log(self.frida_log, f'{msg}\n')
if (self.screenshot_event
and SCREENSHOT_CAPTURED_MSG in msg):
self.screenshot_event.set()
else:
logger.error('[Frida] %s', message)
@@ -287,8 +294,15 @@ class Frida:
self.container_file.write_text(self.app_container)
except frida.InvalidOperationError:
pass
# Migrating defaults to rpc functions
if 'ssl_pinning' in self.defaults:
api.bypass_ios_ssl_pinning()
if not self.extras:
return
screenshot_action = self.extras.get('screenshot_action')
if screenshot_action == 'capture':
api.get_screenshot()
raction = self.extras.get('rclass_action')
rclass = self.extras.get('rclass_name')
rclass_pattern = self.extras.get('rclass_pattern')
+296
View File
@@ -0,0 +1,296 @@
# -*- coding: utf_8 -*-
"""iOS helpers."""
import logging
from threading import Thread
from pathlib import Path
from django.conf import settings
from mobsf.DynamicAnalyzer.tools.webproxy import (
get_http_tools_url,
start_proxy,
stop_httptools,
)
from mobsf.MobSF.utils import (
get_md5,
python_dict,
)
from mobsf.DynamicAnalyzer.views.ios.corellium_ssh import (
ssh_jumphost_reverse_port_forward,
)
from mobsf.StaticAnalyzer.models import StaticAnalyzerIOS
logger = logging.getLogger(__name__)
def get_local_ipa_list():
"""Get local ipa list."""
scan_apps = []
ipas = StaticAnalyzerIOS.objects.filter(
FILE_NAME__endswith='.ipa')
for ipa in reversed(ipas):
bundle_hash = get_md5(ipa.BUNDLE_ID.encode('utf-8'))
frida_dump = Path(
settings.UPLD_DIR) / bundle_hash / 'mobsf_dump_file.txt'
macho = python_dict(ipa.MACHO_ANALYSIS)
encrypted = False
if (macho
and macho.get('encrypted')
and macho.get('encrypted').get('is_encrypted')):
encrypted = macho['encrypted']['is_encrypted']
temp_dict = {
'MD5': ipa.MD5,
'APP_NAME': ipa.APP_NAME,
'APP_VERSION': ipa.APP_VERSION,
'FILE_NAME': ipa.FILE_NAME,
'BUNDLE_ID': ipa.BUNDLE_ID,
'BUNDLE_HASH': bundle_hash,
'ENCRYPTED': encrypted,
'DYNAMIC_REPORT_EXISTS': frida_dump.exists(),
}
scan_apps.append(temp_dict)
return scan_apps
def get_ios_model_mapping():
"""Get iOS model mapping."""
return {
# iPhone Simulator
'i386': 'iPhone Simulator',
'x86_64': 'iPhone Simulator',
'arm64': 'iPhone Simulator',
# iPhone Models
'iPhone1,1': 'iPhone',
'iPhone1,2': 'iPhone 3G',
'iPhone2,1': 'iPhone 3GS',
'iPhone3,1': 'iPhone 4',
'iPhone3,2': 'iPhone 4 GSM Rev A',
'iPhone3,3': 'iPhone 4 CDMA',
'iPhone4,1': 'iPhone 4S',
'iPhone5,1': 'iPhone 5 (GSM)',
'iPhone5,2': 'iPhone 5 (GSM+CDMA)',
'iPhone5,3': 'iPhone 5C (GSM)',
'iPhone5,4': 'iPhone 5C (Global)',
'iPhone6,1': 'iPhone 5S (GSM)',
'iPhone6,2': 'iPhone 5S (Global)',
'iPhone7,1': 'iPhone 6 Plus',
'iPhone7,2': 'iPhone 6',
'iPhone8,1': 'iPhone 6s',
'iPhone8,2': 'iPhone 6s Plus',
'iPhone8,4': 'iPhone SE (GSM)',
'iPhone9,1': 'iPhone 7',
'iPhone9,2': 'iPhone 7 Plus',
'iPhone9,3': 'iPhone 7',
'iPhone9,4': 'iPhone 7 Plus',
'iPhone10,1': 'iPhone 8',
'iPhone10,2': 'iPhone 8 Plus',
'iPhone10,3': 'iPhone X Global',
'iPhone10,4': 'iPhone 8',
'iPhone10,5': 'iPhone 8 Plus',
'iPhone10,6': 'iPhone X GSM',
'iPhone11,2': 'iPhone XS',
'iPhone11,4': 'iPhone XS Max',
'iPhone11,6': 'iPhone XS Max Global',
'iPhone11,8': 'iPhone XR',
'iPhone12,1': 'iPhone 11',
'iPhone12,3': 'iPhone 11 Pro',
'iPhone12,5': 'iPhone 11 Pro Max',
'iPhone12,8': 'iPhone SE 2nd Gen',
'iPhone13,1': 'iPhone 12 Mini',
'iPhone13,2': 'iPhone 12',
'iPhone13,3': 'iPhone 12 Pro',
'iPhone13,4': 'iPhone 12 Pro Max',
'iPhone14,2': 'iPhone 13 Pro',
'iPhone14,3': 'iPhone 13 Pro Max',
'iPhone14,4': 'iPhone 13 Mini',
'iPhone14,5': 'iPhone 13',
'iPhone14,6': 'iPhone SE 3rd Gen',
'iPhone14,7': 'iPhone 14',
'iPhone14,8': 'iPhone 14 Plus',
'iPhone15,2': 'iPhone 14 Pro',
'iPhone15,3': 'iPhone 14 Pro Max',
'iPhone15,4': 'iPhone 15',
'iPhone15,5': 'iPhone 15 Plus',
'iPhone16,1': 'iPhone 15 Pro',
'iPhone16,2': 'iPhone 15 Pro Max',
'iPhone17,1': 'iPhone 16 Pro',
'iPhone17,2': 'iPhone 16 Pro Max',
'iPhone17,3': 'iPhone 16',
'iPhone17,4': 'iPhone 16 Plus',
'iPhone17,5': 'iPhone 16e',
# iPod Models
'iPod1,1': '1st Gen iPod',
'iPod2,1': '2nd Gen iPod',
'iPod3,1': '3rd Gen iPod',
'iPod4,1': '4th Gen iPod',
'iPod5,1': '5th Gen iPod',
'iPod7,1': '6th Gen iPod',
'iPod9,1': '7th Gen iPod',
# iPad Models
'iPad1,1': 'iPad',
'iPad1,2': 'iPad 3G',
'iPad2,1': '2nd Gen iPad',
'iPad2,2': '2nd Gen iPad GSM',
'iPad2,3': '2nd Gen iPad CDMA',
'iPad2,4': '2nd Gen iPad New Revision',
'iPad3,1': '3rd Gen iPad',
'iPad3,2': '3rd Gen iPad CDMA',
'iPad3,3': '3rd Gen iPad GSM',
'iPad2,5': 'iPad mini',
'iPad2,6': 'iPad mini GSM+LTE',
'iPad2,7': 'iPad mini CDMA+LTE',
'iPad3,4': '4th Gen iPad',
'iPad3,5': '4th Gen iPad GSM+LTE',
'iPad3,6': '4th Gen iPad CDMA+LTE',
'iPad4,1': 'iPad Air (WiFi)',
'iPad4,2': 'iPad Air (GSM+CDMA)',
'iPad4,3': '1st Gen iPad Air (China)',
'iPad4,4': 'iPad mini Retina (WiFi)',
'iPad4,5': 'iPad mini Retina (GSM+CDMA)',
'iPad4,6': 'iPad mini Retina (China)',
'iPad4,7': 'iPad mini 3 (WiFi)',
'iPad4,8': 'iPad mini 3 (GSM+CDMA)',
'iPad4,9': 'iPad Mini 3 (China)',
'iPad5,1': 'iPad mini 4 (WiFi)',
'iPad5,2': '4th Gen iPad mini (WiFi+Cellular)',
'iPad5,3': 'iPad Air 2 (WiFi)',
'iPad5,4': 'iPad Air 2 (Cellular)',
'iPad6,3': 'iPad Pro (9.7 inch, WiFi)',
'iPad6,4': 'iPad Pro (9.7 inch, WiFi+LTE)',
'iPad6,7': 'iPad Pro (12.9 inch, WiFi)',
'iPad6,8': 'iPad Pro (12.9 inch, WiFi+LTE)',
'iPad6,11': 'iPad (2017)',
'iPad6,12': 'iPad (2017)',
'iPad7,1': 'iPad Pro 2nd Gen (WiFi)',
'iPad7,2': 'iPad Pro 2nd Gen (WiFi+Cellular)',
'iPad7,3': 'iPad Pro 10.5-inch 2nd Gen',
'iPad7,4': 'iPad Pro 10.5-inch 2nd Gen',
'iPad7,5': 'iPad 6th Gen (WiFi)',
'iPad7,6': 'iPad 6th Gen (WiFi+Cellular)',
'iPad7,11': 'iPad 7th Gen 10.2-inch (WiFi)',
'iPad7,12': 'iPad 7th Gen 10.2-inch (WiFi+Cellular)',
'iPad8,1': 'iPad Pro 11 inch 3rd Gen (WiFi)',
'iPad8,2': 'iPad Pro 11 inch 3rd Gen (1TB, WiFi)',
'iPad8,3': 'iPad Pro 11 inch 3rd Gen (WiFi+Cellular)',
'iPad8,4': 'iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular)',
'iPad8,5': 'iPad Pro 12.9 inch 3rd Gen (WiFi)',
'iPad8,6': 'iPad Pro 12.9 inch 3rd Gen (1TB, WiFi)',
'iPad8,7': 'iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular)',
'iPad8,8': 'iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular)',
'iPad8,9': 'iPad Pro 11 inch 4th Gen (WiFi)',
'iPad8,10': 'iPad Pro 11 inch 4th Gen (WiFi+Cellular)',
'iPad8,11': 'iPad Pro 12.9 inch 4th Gen (WiFi)',
'iPad8,12': 'iPad Pro 12.9 inch 4th Gen (WiFi+Cellular)',
'iPad11,1': 'iPad mini 5th Gen (WiFi)',
'iPad11,2': 'iPad mini 5th Gen',
'iPad11,3': 'iPad Air 3rd Gen (WiFi)',
'iPad11,4': 'iPad Air 3rd Gen',
'iPad11,6': 'iPad 8th Gen (WiFi)',
'iPad11,7': 'iPad 8th Gen (WiFi+Cellular)',
'iPad12,1': 'iPad 9th Gen (WiFi)',
'iPad12,2': 'iPad 9th Gen (WiFi+Cellular)',
'iPad14,1': 'iPad mini 6th Gen (WiFi)',
'iPad14,2': 'iPad mini 6th Gen (WiFi+Cellular)',
'iPad13,1': 'iPad Air 4th Gen (WiFi)',
'iPad13,2': 'iPad Air 4th Gen (WiFi+Cellular)',
'iPad13,4': 'iPad Pro 11 inch 5th Gen',
'iPad13,5': 'iPad Pro 11 inch 5th Gen',
'iPad13,6': 'iPad Pro 11 inch 5th Gen',
'iPad13,7': 'iPad Pro 11 inch 5th Gen',
'iPad13,8': 'iPad Pro 12.9 inch 5th Gen',
'iPad13,9': 'iPad Pro 12.9 inch 5th Gen',
'iPad13,10': 'iPad Pro 12.9 inch 5th Gen',
'iPad13,11': 'iPad Pro 12.9 inch 5th Gen',
'iPad13,16': 'iPad Air 5th Gen (WiFi)',
'iPad13,17': 'iPad Air 5th Gen (WiFi+Cellular)',
'iPad13,18': 'iPad 10th Gen',
'iPad13,19': 'iPad 10th Gen',
'iPad14,3': 'iPad Pro 11 inch 4th Gen',
'iPad14,4': 'iPad Pro 11 inch 4th Gen',
'iPad14,5': 'iPad Pro 12.9 inch 6th Gen',
'iPad14,6': 'iPad Pro 12.9 inch 6th Gen',
'iPad14,8': 'iPad Air 11 inch 6th Gen (WiFi)',
'iPad14,9': 'iPad Air 11 inch 6th Gen (WiFi+Cellular)',
'iPad14,10': 'iPad Air 13 inch 6th Gen (WiFi)',
'iPad14,11': 'iPad Air 13 inch 6th Gen (WiFi+Cellular)',
'iPad15,3': 'iPad Air 11-inch 7th Gen (WiFi)',
'iPad15,4': 'iPad Air 11-inch 7th Gen (WiFi+Cellular)',
'iPad15,5': 'iPad Air 13-inch 7th Gen (WiFi)',
'iPad15,6': 'iPad Air 13-inch 7th Gen (WiFi+Cellular)',
'iPad15,7': 'iPad 11th Gen (WiFi)',
'iPad15,8': 'iPad 11th Gen (WiFi+Cellular)',
'iPad16,1': 'iPad mini 7th Gen (WiFi)',
'iPad16,2': 'iPad mini 7th Gen (WiFi+Cellular)',
'iPad16,3': 'iPad Pro 11 inch 5th Gen',
'iPad16,4': 'iPad Pro 11 inch 5th Gen',
'iPad16,5': 'iPad Pro 12.9 inch 7th Gen',
'iPad16,6': 'iPad Pro 12.9 inch 7th Gen',
# Apple Watch Models
'Watch1,1': 'Apple Watch 38mm case',
'Watch1,2': 'Apple Watch 42mm case',
'Watch2,6': 'Apple Watch Series 1 38mm case',
'Watch2,7': 'Apple Watch Series 1 42mm case',
'Watch2,3': 'Apple Watch Series 2 38mm case',
'Watch2,4': 'Apple Watch Series 2 42mm case',
'Watch3,1': 'Apple Watch Series 3 38mm case (GPS+Cellular)',
'Watch3,2': 'Apple Watch Series 3 42mm case (GPS+Cellular)',
'Watch3,3': 'Apple Watch Series 3 38mm case (GPS)',
'Watch3,4': 'Apple Watch Series 3 42mm case (GPS)',
'Watch4,1': 'Apple Watch Series 4 40mm case (GPS)',
'Watch4,2': 'Apple Watch Series 4 44mm case (GPS)',
'Watch4,3': 'Apple Watch Series 4 40mm case (GPS+Cellular)',
'Watch4,4': 'Apple Watch Series 4 44mm case (GPS+Cellular)',
'Watch5,1': 'Apple Watch Series 5 40mm case (GPS)',
'Watch5,2': 'Apple Watch Series 5 44mm case (GPS)',
'Watch5,3': 'Apple Watch Series 5 40mm case (GPS+Cellular)',
'Watch5,4': 'Apple Watch Series 5 44mm case (GPS+Cellular)',
'Watch5,9': 'Apple Watch SE 40mm case (GPS)',
'Watch5,10': 'Apple Watch SE 44mm case (GPS)',
'Watch5,11': 'Apple Watch SE 40mm case (GPS+Cellular)',
'Watch5,12': 'Apple Watch SE 44mm case (GPS+Cellular)',
'Watch6,1': 'Apple Watch Series 6 40mm case (GPS)',
'Watch6,2': 'Apple Watch Series 6 44mm case (GPS)',
'Watch6,3': 'Apple Watch Series 6 40mm case (GPS+Cellular)',
'Watch6,4': 'Apple Watch Series 6 44mm case (GPS+Cellular)',
'Watch6,6': 'Apple Watch Series 7 41mm case (GPS)',
'Watch6,7': 'Apple Watch Series 7 45mm case (GPS)',
'Watch6,8': 'Apple Watch Series 7 41mm case (GPS+Cellular)',
'Watch6,9': 'Apple Watch Series 7 45mm case (GPS+Cellular)',
'Watch6,10': 'Apple Watch SE 40mm case (GPS)',
'Watch6,11': 'Apple Watch SE 44mm case (GPS)',
'Watch6,12': 'Apple Watch SE 40mm case (GPS+Cellular)',
'Watch6,13': 'Apple Watch SE 44mm case (GPS+Cellular)',
'Watch6,14': 'Apple Watch Series 8 41mm case (GPS)',
'Watch6,15': 'Apple Watch Series 8 45mm case (GPS)',
'Watch6,16': 'Apple Watch Series 8 41mm case (GPS+Cellular)',
'Watch6,17': 'Apple Watch Series 8 45mm case (GPS+Cellular)',
'Watch6,18': 'Apple Watch Ultra',
'Watch7,1': 'Apple Watch Series 9 41mm case (GPS)',
'Watch7,2': 'Apple Watch Series 9 45mm case (GPS)',
'Watch7,3': 'Apple Watch Series 9 41mm case (GPS+Cellular)',
'Watch7,4': 'Apple Watch Series 9 45mm case (GPS+Cellular)',
'Watch7,5': 'Apple Watch Ultra 2',
'Watch7,8': 'Apple Watch Series 10 42mm case (GPS)',
'Watch7,9': 'Apple Watch Series 10 46mm case (GPS)',
'Watch7,10': 'Apple Watch Series 10 42mm case (GPS+Cellular)',
'Watch7,11': 'Apple Watch Series 10 46mm (GPS+Cellular)',
}
def configure_proxy(request, project, ci=None):
"""Configure HTTPS Proxy."""
proxy_port = settings.PROXY_PORT
logger.info('Starting HTTPS Proxy on %s', proxy_port)
stop_httptools(get_http_tools_url(request))
start_proxy(proxy_port, project)
if ci:
# Remote Port forward for HTTPS Proxy
logger.info('Starting Remote Port Forward over SSH')
Thread(target=ssh_jumphost_reverse_port_forward,
args=(ci.get_ssh_connection_string(),),
daemon=True).start()
+5 -4
View File
@@ -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
+6 -6
View File
@@ -18,13 +18,13 @@ from mobsf.install.windows.setup import windows_config_local
logger = logging.getLogger(__name__)
VERSION = '4.4.6'
VERSION = '4.5.0'
BANNER = r"""
__ __ _ ____ _____ _ _ _ _
| \/ | ___ | |__/ ___|| ___|_ _| || | | || |
| |\/| |/ _ \| '_ \___ \| |_ \ \ / / || |_| || |_
| | | | (_) | |_) |__) | _| \ V /|__ _|__ _|
|_| |_|\___/|_.__/____/|_| \_/ |_|(_) |_|
__ __ _ ____ _____ _ _ ____
| \/ | ___ | |__/ ___|| ___|_ _| || | | ___|
| |\/| |/ _ \| '_ \___ \| |_ \ \ / / || |_ |___ \
| | | | (_) | |_) |__) | _| \ V /|__ _| ___) |
|_| |_|\___/|_.__/____/|_| \_/ |_|(_)____/
""" # noqa: W291
# ASCII Font: Standard
+5
View File
@@ -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)
+10
View File
@@ -532,6 +532,16 @@ else:
# if VT_UPLOAD is set to True.
# ===============================================
# =======IOS DYNAMIC ANALYSIS SETTINGS===========
# Should be SSH IP:PORT, example: 192.168.1.100:22
# Field also supports multiple devices, example: 192.168.1.100:22,192.168.1.101:22
IOS_ANALYZER_IDENTIFIERS = os.getenv('MOBSF_IOS_ANALYZER_IDENTIFIERS', '')
# SSH credentials for jailbroken iOS device (USB or WiFi path)
# Defaults match the standard jailbreak SSH default (root/alpine)
IOS_SSH_USER = os.getenv('MOBSF_IOS_SSH_USER', 'root')
IOS_SSH_PASSWORD = os.getenv('MOBSF_IOS_SSH_PASSWORD', 'alpine')
# ==============================================
# =======IOS DYNAMIC ANALYSIS CORELLIUM SETTINGS===========
CORELLIUM_API_DOMAIN = os.getenv('MOBSF_CORELLIUM_API_DOMAIN', '')
CORELLIUM_API_KEY = os.getenv('MOBSF_CORELLIUM_API_KEY', '')
CORELLIUM_PROJECT_ID = os.getenv('MOBSF_CORELLIUM_PROJECT_ID', '')
+75 -2
View File
@@ -15,6 +15,12 @@ from mobsf.DynamicAnalyzer.views.ios import (
report as ios_view_report,
tests_frida as ios_tests_frida,
)
from mobsf.DynamicAnalyzer.views.ios.device import (
dynamic_analyzer as ios_device,
)
from mobsf.DynamicAnalyzer.views.ios.device import (
report as device_report,
)
from mobsf.MobSF import utils
from mobsf.MobSF.security import (
init_exec_hooks,
@@ -29,6 +35,7 @@ from mobsf.MobSF.views import (
from mobsf.MobSF.views.api import api_static_analysis as api_sz
from mobsf.MobSF.views.api import api_android_dynamic_analysis as api_dz
from mobsf.MobSF.views.api import api_ios_dynamic_analysis as api_idz
from mobsf.MobSF.views.api import api_ios_device_analysis as api_iddz
from mobsf.StaticAnalyzer import tests
from mobsf.StaticAnalyzer.views.common import (
appsec,
@@ -123,7 +130,7 @@ urlpatterns = [
re_path(r'^api/v1/frida/list_scripts$', api_dz.api_list_frida_scripts),
re_path(r'^api/v1/frida/get_script$', api_dz.api_get_script_content),
re_path(r'^api/v1/dynamic/view_source$', api_dz.api_dynamic_view_file),
# iOS Specific
# iOS Corellium
re_path(r'^api/v1/ios/corellium_supported_models$',
api_idz.api_corellium_get_supported_models),
re_path(r'^api/v1/ios/corellium_ios_versions$',
@@ -177,6 +184,33 @@ urlpatterns = [
# Frida
re_path(r'^api/v1/frida/ios_instrument$', api_idz.api_ios_instrument),
re_path(r'^api/v1/dynamic/ios_report_json$', api_idz.api_ios_view_report),
# iOS Jailbroken Device
re_path(r'^api/v1/ios/device_dynamic_analysis$',
api_iddz.api_device_dynamic_analysis),
re_path(r'^api/v1/ios/device_get$',
api_iddz.api_device_get),
re_path(r'^api/v1/ios/device_install_ipa$',
api_iddz.api_device_install_ipa),
re_path(r'^api/v1/ios/device_dynamic_analyzer$',
api_iddz.api_device_dynamic_analyzer),
re_path(r'^api/v1/ios/device_file_upload$',
api_iddz.api_device_file_upload),
re_path(r'^api/v1/ios/device_ssh_execute$',
api_iddz.api_device_ssh_execute),
re_path(r'^api/v1/ios/device_instrument$',
api_iddz.api_device_instrument),
re_path(r'^api/v1/ios/device_system_logs$',
api_iddz.api_device_system_logs),
re_path(r'^api/v1/ios/device_ps$',
api_iddz.api_device_ps),
re_path(r'^api/v1/ios/device_file_download$',
api_iddz.api_device_file_download),
re_path(r'^api/v1/ios/device_api_monitor$',
api_iddz.api_device_api_monitor),
re_path(r'^api/v1/ios/device_download_app_data$',
api_iddz.api_device_download_app_data),
re_path(r'^api/v1/ios/device_report_json$',
api_iddz.api_device_report_json),
]
if settings.API_ONLY == '0':
urlpatterns.extend([
@@ -399,7 +433,46 @@ if settings.API_ONLY == '0':
re_path(fr'^ios/view_report/{bundle_id_regex}',
ios_view_report.ios_view_report,
name='ios_view_report'),
# iOS Device
re_path(r'^ios/dynamic_analysis_device/$',
ios_device.dynamic_analysis_device,
name='dynamic_ios_device'),
re_path(r'^ios/ios_device/$',
ios_device.get_ios_device,
name='get_ios_device'),
re_path(r'^ios/install_ipa_device/$',
ios_device.install_ipa_device,
name='install_ipa_device'),
re_path(r'^ios/dynamic_analyzer_device/$',
ios_device.dynamic_analyzer_device,
name='dynamic_analyzer_device'),
re_path(r'^ios/upload_file_device/$',
ios_device.upload_file_device,
name='upload_file_device'),
re_path(r'^ios/ssh_execute_device/$',
ios_device.ssh_execute_device,
name='ssh_execute_device'),
re_path(r'^ios/instrument_device/$',
ios_device.ios_instrument_device,
name='ios_instrument_device'),
re_path(r'^ios/system_logs_device/$',
ios_device.system_logs_device,
name='system_logs_device'),
re_path(r'^ios/ps_device/$',
ios_device.ps_device,
name='ps_device'),
re_path(r'^ios/download_file_device/$',
device_report.download_file_device,
name='download_file_device'),
re_path(r'^ios/api_monitor/$',
device_report.ios_api_monitor,
name='ios_api_monitor'),
re_path(fr'^ios/download_data_device/{bundle_id_regex}',
device_report.download_data_device,
name='download_data_device'),
re_path(fr'^ios/view_report_device/{bundle_id_regex}',
device_report.view_report_device,
name='view_report_device'),
# Test
re_path(r'^tests/$', tests.start_test),
])
+16
View File
@@ -63,6 +63,9 @@ GOOGLE_API_KEY_REGEX = re.compile(r'AIza[0-9A-Za-z-_]{35}$')
GOOGLE_APP_ID_REGEX = re.compile(r'\d{1,2}:\d{1,50}:android:[a-f0-9]{1,50}')
PKG_REGEX = re.compile(
r'package\s+([a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*);')
IOS_DEVICE_ID_REGEX = re.compile(r'^[a-fA-F0-9-]{20,40}$')
SSH_DEVICE_ID_REGEX = re.compile(
r'^(?:\[(?P<ipv6>[^\]]+)\]|(?P<host>[^:\[\]]+)):(?P<port>\d{1,5})$')
class Color(object):
@@ -1001,3 +1004,16 @@ def set_permissions(path):
item.chmod(perm_file)
except Exception:
pass
def parse_host_port(text):
# Match [IPv6]:port OR host:port
ipv6_match = re.match(r'^\[(.+)\]:(\d+)$', text)
if ipv6_match:
return ipv6_match.group(1), int(ipv6_match.group(2))
ipv4_match = re.match(r'^([^:]+):(\d+)$', text)
if ipv4_match:
return ipv4_match.group(1), int(ipv4_match.group(2))
raise ValueError('Input must be in format host:port or [IPv6]:port')
@@ -0,0 +1,211 @@
# -*- coding: utf_8 -*-
"""MobSF REST API V 1 - iOS Jailbroken Device."""
from django.views.decorators.csrf import csrf_exempt
from mobsf.MobSF.views.helpers import request_method
from mobsf.MobSF.views.api.api_middleware import make_api_response
from mobsf.DynamicAnalyzer.views.ios.device import (
dynamic_analyzer as ios_device,
report as device_report,
)
FAILED = 'failed'
ERROR = 'error'
DEVICE_ID = 'device_id'
BUNDLE_ID = 'bundle_id'
@request_method(['POST'])
@csrf_exempt
def api_device_dynamic_analysis(request):
"""POST - iOS Device Dynamic Analysis Entrypoint."""
if DEVICE_ID not in request.POST:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.dynamic_analysis_device(request, True)
if ERROR in resp:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_get(request):
"""POST - Get iOS Device Details and List Apps."""
if DEVICE_ID not in request.POST:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.get_ios_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_install_ipa(request):
"""POST - Install IPA on iOS Device."""
params = {DEVICE_ID, 'checksum'}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.install_ipa_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_dynamic_analyzer(request):
"""POST - iOS Device Dynamic Analyzer."""
params = {DEVICE_ID, BUNDLE_ID}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.dynamic_analyzer_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_file_upload(request):
"""POST - Upload file to iOS Device."""
if DEVICE_ID not in request.POST:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
if 'file' not in request.FILES:
return make_api_response(
{'error': 'Missing File'}, 422)
resp = ios_device.upload_file_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_ssh_execute(request):
"""POST - Execute commands on iOS Device over SSH."""
params = {DEVICE_ID, 'cmd'}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.ssh_execute_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_instrument(request):
"""POST - Frida Instrument on iOS Device."""
params = {
DEVICE_ID,
BUNDLE_ID,
'default_hooks',
'dump_hooks',
'auxiliary_hooks',
'frida_code'}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.ios_instrument_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_system_logs(request):
"""POST - Get system logs from iOS Device."""
if DEVICE_ID not in request.POST:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.system_logs_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_ps(request):
"""POST - Get running processes on iOS Device."""
if DEVICE_ID not in request.POST:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = ios_device.ps_device(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_file_download(request):
"""POST - Download file from iOS Device."""
params = {DEVICE_ID, 'file'}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = device_report.download_file_device(request, True)
if resp.get('Content-Disposition'):
# file http response
return resp
elif resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_api_monitor(request):
"""POST - iOS Device API Monitor."""
if 'checksum' not in request.POST:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = device_report.ios_api_monitor(request, True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_download_app_data(request):
"""POST - Download Application Data from iOS Device."""
params = {DEVICE_ID, BUNDLE_ID}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = device_report.download_data_device(
request,
request.POST[BUNDLE_ID],
True)
if resp.get('status') == FAILED:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
@request_method(['POST'])
@csrf_exempt
def api_device_report_json(request):
"""POST - iOS Device Dynamic Analysis Report."""
params = {DEVICE_ID, BUNDLE_ID}
if set(request.POST) < params:
return make_api_response(
{'error': 'Missing Parameters'}, 422)
resp = device_report.view_report_device(
request,
request.POST[BUNDLE_ID],
True)
if 'error' in resp:
return make_api_response(resp, 500)
return make_api_response(resp, 200)
+3 -1
View File
@@ -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 {
@@ -12,7 +12,7 @@
<link rel="stylesheet" href="{% static "codemirror/lint.css" %}" type="text/css">
<link rel="stylesheet" href="{% static "others/css/spinner.css" %}">
<style>
textarea {
#stat {
width: 100%;
height: {% if android_version < 5 %} 630px; {% else %} 170px; {% endif %}
-moz-border-bottom-colors: none;
@@ -33,33 +33,43 @@
padding: 5px 8px;
transition: background-color 0.2s ease 0s;
}
textarea:focus {
#stat:focus {
background: none repeat scroll 0 0 #FFFFFF;
outline-width: 0;
}
.highlight { border: solid 2px #1e6dff;}
/* Complete isolation for CodeMirror */
#codemirror-isolation-container {
zoom: 1 !important;
transform: scale(1.25) !important; /* Counter the 0.8 zoom */
transform-origin: top left !important;
width: 80% !important; /* Adjust for scale */
height: 80% !important; /* Adjust for scale */
position: relative !important;
overflow: hidden !important;
#code-editor{
transform: scale(1.25) !important;
transform-origin: top left;
width: 80%;
height: auto;
overflow: auto;
max-height: 600px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
background: #f8f9fa;
margin-bottom: 150px;
position: relative;
z-index: 1;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
#code-editor::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.CodeMirror {
height: 610px;
font-size: 11px;
border: 1px solid #eee;
}
.frida-pre {
font-family: "courier new", courier, monospace;
font-size: 11px;
}
.frida-pre {
font-family: "courier new", courier, monospace;
font-size: 11px;
}
#start_activity{
margin-top: 5px;
@@ -364,29 +374,22 @@
</div>
<!-- /.card-header -->
<div class="card-body">
<div class="row">
<div class="col-12">
<div id="codemirror-isolation-container">
<div id="code-editor">
<textarea id="code-js" rows="100">
Java.perform(function() {
// Use send() for logging
});
Java.perform(function() {
// Use send() for logging
});
</textarea>
</div>
</div>
<div class="col-12">
<br/>
<div class="form-group">
<span class="box-title">Available Scripts (Use CTRL to choose multiple) </span>
<button id="loadscript" type="submit" class="btn btn-primary btn-sm">Load</button>
</div>
<div class="form-group">
<select id="fd_scs" size="10" multiple="" class="form-control">
</select>
</div>
</div>
</div>
<!-- end row-->
<br/>
<div class="form-group">
<span class="box-title">Available Scripts (Use CTRL to choose multiple) </span>
<button id="loadscript" type="submit" class="btn btn-primary btn-sm">Load</button>
</div>
<div class="form-group">
<select id="fd_scs" size="10" multiple="" class="form-control">
</select>
</div>
</div>
<!-- /.card-body -->
@@ -647,7 +650,7 @@ function load_frida_others(){
action('{% url 'list_frida_scripts' %}', {device: 'android'}, function(json) {
if (json.status==="ok"){
json.files.forEach(function(script) {
$('#fd_scs').append('<option value=' + escape(script) + '>' + escape(script) + '</option>');
$('<option>').val(script).text(script).appendTo('#fd_scs');
});
}
});
@@ -740,7 +743,9 @@ var Terminal = Terminal || function (cmdLineContainer, outputContainer) {
if (e.keyCode == 38 || e.keyCode == 40) {
this.value = history_[histpos_] ? history_[histpos_] : histtemp_;
this.value = this.value; // Sets cursor to end of input.
setTimeout(() => {
this.setSelectionRange(this.value.length, this.value.length);
}, 0); // Sets cursor to end of input.
}
}
}
@@ -757,15 +762,43 @@ var Terminal = Terminal || function (cmdLineContainer, outputContainer) {
output_.insertAdjacentHTML('beforeEnd', '<p>' + html + '</p>');
}
// Cross-browser impl to get document's height.
function getDocHeight_() {
var d = document;
return Math.max(
Math.max(d.body.scrollHeight, d.documentElement.scrollHeight),
Math.max(d.body.offsetHeight, d.documentElement.offsetHeight),
Math.max(d.body.clientHeight, d.documentElement.clientHeight)
);
function scrollToShellBottom(){
// Scroll to shell bottom and ensure shell is completely visible
var myElement = document.getElementById('shell');
// Scroll the shell content to the bottom
myElement.scrollTop = myElement.scrollHeight;
// Ensure the shell element is visible in the viewport
myElement.scrollIntoView({
block: 'end',
behavior: 'smooth',
inline: 'nearest'
});
// For dynamic content, we need to continuously monitor and scroll
// Set up a MutationObserver to watch for content changes
if (!window.shellObserver) {
window.shellObserver = new MutationObserver(function(mutations) {
// When content changes, scroll to keep shell visible
myElement.scrollTop = myElement.scrollHeight;
myElement.scrollIntoView({
block: 'end',
behavior: 'smooth',
inline: 'nearest'
});
});
// Start observing the shell for content changes
window.shellObserver.observe(myElement, {
childList: true,
subtree: true,
characterData: true
});
}
}
function get_comp(comp){
action(document.location.origin + '/get_component/', { component: comp, hash: '{{ hash }}'}, function(json) {
if (json.status==="ok")
@@ -850,7 +883,7 @@ var Terminal = Terminal || function (cmdLineContainer, outputContainer) {
else if (exec_cmd.length > 0)
execute_adb(exec_cmd)
};
window.scrollTo(0, getDocHeight_());
scrollToShellBottom();
this.value = ''; // Clear/setup line for next input.
}
}
@@ -1364,8 +1397,7 @@ function frida_instrument(frida_action, pid=null, new_pkg=null){
if (frida_action == 'ps' && json.message && json.message.length > 0 && Array.isArray(json.message)){
$("#process_select").empty();
json.message.forEach(function(p) {
$('#process_select').append(
'<option value=' + escape(p.pid.toString()) + ',' + escape(p.identifier.toString()) + '>' + escape(p.pid.toString()) + ': ' + escapeHtml(p.name.toString()) + ' - ' + escape(p.identifier.toString()) + '</option>');
$('<option>').val(p.pid.toString() + ',' + p.identifier.toString()).text(p.pid.toString() + ': ' + p.name.toString() + ' - ' + p.identifier.toString()).appendTo('#process_select');
});
$('#frida_attach_mdl').modal('toggle');
} else if (frida_action =='get'){
@@ -0,0 +1,148 @@
{% extends "base/base_layout.html" %}
{% load static %}
{% block sidebar_option %}
sidebar-collapse
{% endblock %}
{% block extra_css %}
<!-- DataTables -->
<link rel="stylesheet" href="{% static "adminlte/plugins/datatables-bs4/dataTables.bootstrap4.min.css" %}">
{% endblock %}
{% block content %}
<div class="content-wrapper">
<div class="content-header">
</div>
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<h3 class="page-header"><strong>iOS API Monitor </strong>- {{ bundle_id }}</h3>
<div id="messages"></br>Data refreshed in every 10 seconds.</div>
<div align="right">Data Snip: <input type="text" id="snip" value="5000" oninput="change_snip(this.value)"></div><br />
<div class="table-responsive">
<table id="autotbl" class="table table-bordered table-hover table-striped" style="width:100%">
<thead>
<tr>
<th>API</th>
<th>DATA</th>
</tr>
</thead>
<tfoot>
<tr>
<th>API</th>
<th>DATA</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<!-- DataTables -->
<script src="{% static "adminlte/plugins/datatables/jquery.dataTables.min.js" %} "></script>
<script src="{% static "adminlte/plugins/datatables-bs4/dataTables.bootstrap4.min.js" %}"></script>
<script type="text/javascript">
var snip = document.getElementById('snip').value;
var tbl;
function change_snip(val){
snip = parseInt(val);
tbl.ajax.reload();
}
// HTML escape function
function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Function to flatten and format data in one pass
function formatFlattenedData(data) {
if (!data || typeof data !== 'object') {
return escapeHtml(JSON.stringify(data));
}
const formatted = [];
function flattenAndFormat(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
// Recursively process nested objects
flattenAndFormat(value);
} else if (Array.isArray(value)) {
// Handle arrays
if (value.length > 0 && typeof value[0] === 'object') {
// Array of objects - process each object
value.forEach(item => {
if (typeof item === 'object' && item !== null) {
flattenAndFormat(item);
} else {
formatted.push(`<strong>${escapeHtml(key)}</strong>: ${escapeHtml(String(item))}`);
}
});
} else {
// Array of primitives
formatted.push(`<strong>${escapeHtml(key)}</strong>: ${escapeHtml(JSON.stringify(value))}`);
}
} else {
// Handle primitive values
formatted.push(`<strong>${escapeHtml(key)}</strong>: ${escapeHtml(String(value))}`);
}
}
}
}
flattenAndFormat(data);
return formatted.join('<br>');
}
$(document).ready( function () {
$.fn.dataTable.ext.errMode = 'none';
tbl = $('#autotbl').DataTable( {
ajax: '{% url "ios_api_monitor" %}?checksum={{ checksum }}&stream=1',
deferRender: true,
scroller: true,
scrollX: true,
searching: true,
paging: false,
info: true,
processing: true,
serverSide: false,
columns: [
{ data: "api" },
{ data: "data" },
],
columnDefs: [ {
targets: [1],
render: function (data, type, row) {
const formattedData = formatFlattenedData(data);
return formattedData.length > snip ?
formattedData.slice(0, snip) + "..." :
formattedData;
}
}
]
});
setInterval( function () {
tbl.ajax.reload(null, false); // false = don't reset paging
}, 10000 );
});
</script>
{% endblock %}
@@ -0,0 +1,335 @@
{% extends "base/base_layout.html" %}
{% load static %}
{% block sidebar_option %}
sidebar-collapse
{% endblock %}
{% block extra_css %}
<!-- DataTables -->
<link rel="stylesheet" href="{% static "adminlte/plugins/datatables-bs4/dataTables.bootstrap4.min.css" %}">
<link rel="stylesheet" href="{% static "others/css/spinner.css" %}">
<link href="{% static "adminlte/plugins/sweetalert2/sweetalert2.min.css" %}" rel="stylesheet">
<style>
#app_icon{
width: 64px;
height: 64px;
}
</style>
{% endblock %}
{% block content %}
<div class="content-wrapper">
<div class="content-header">
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="card bg-gradient-primary">
<!-- /.card-header -->
<div class="card-body">
<div class="row">
<div class="col-md-12">
<h4>MobSF iOS Dynamic Analyzer</h4>
<data id="choosen_device" value="{{ selected_device }}"></data>
<div class="card-body">
<div class="table-responsive">
<p>
<h6><strong> Choose a device for Dynamic Analysis: </strong></h6>
<select class="form-control" id="ios_dynamic"></select>
</p>
</div>
</div>
</div>
</div>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
<!-- /.col -->
<div class="col-md-12">
<div class="card card-default">
<div class="card-header">
<h3 class="card-title"><i class="fa fa-rocket"></i> MobSF Dynamic Analysis</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<h4> Apps in Device</h4>
<table id="in_device" class="table table-bordered table-hover table-striped">
<tbody>
</tbody>
</table>
</br>
<h4> Apps Available</h4>
<table class="dattbl table table-bordered table-hover table-striped">
<thead>
<tr>
<th>APP</th>
<th>FILE NAME</th>
<th>BUNDLE ID</th>
<th>ACTION</th>
</tr>
</thead>
<tbody>
{% if apps %}
{% for e in apps %}
<tr>
<td align="center">
<img id="app_icon" src="/download/{{ e.MD5 }}-icon.png"/>
<br/><strong>{{ e.APP_NAME }} - {{ e.APP_VERSION }}</strong>
</td>
<td>
{{ e.FILE_NAME }}
</td>
<td>
{{ e.BUNDLE_ID }}
</td>
<td align="center">
{% if e.ENCRYPTED %}
<span class="badge badge-danger">ENCRYPTED IPA</span></br>
<small>You need decrypted IPA for dynamic analysis.</small>
{% else %}
<p>
<a class="btn btn-success upload-install-btn disabled" onclick="upload_and_install('{{ e.MD5 }}')" data-checksum="{{ e.MD5 }}"><i class="fab fa-apple"></i> Upload &amp; Install</a>
</p>
<p>
<a class="btn btn-info {% if not e.DYNAMIC_REPORT_EXISTS %}disabled{% endif %}" href="{% url 'view_report_device' bundle_id=e.BUNDLE_ID %}" onclick="dynamic_loader()"><i class="fa fa-mobile"></i> View Report </a>
</p>
{% endif %}
</td>
</tr>
{% endfor %}
{% endif %}
</tbody>
</table>
</div>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
<!-- /.col -->
</div>
</div>
</div>
</div>
</div>
</div>
<!--Loader-->
<div class="hidden loading">
<div class='uil-ring-css' style='transform:scale(0.79);'>
<div></div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<!-- DataTables -->
<script src="{% static "adminlte/plugins/datatables/jquery.dataTables.min.js" %} "></script>
<script src="{% static "adminlte/plugins/datatables-bs4/dataTables.bootstrap4.min.js" %}"></script>
<script src="{% static "adminlte/plugins/sweetalert2/sweetalert2.min.js" %}"></script>
<script>
// Escape HTML
function escapeHtml(unsafe){
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function dynamic_loader(){
var loadingOverlay = document.querySelector('.loading');
loadingOverlay.classList.remove('hidden');
}
function stop_loader(){
var loadingOverlay = document.querySelector('.loading');
loadingOverlay.classList.add('hidden');
}
// Datatables
$(function () {
// Datatable
$('.dattbl').DataTable({
"paging": true,
"lengthChange": false,
"searching": true,
"ordering": false,
"info": true,
"autoWidth": true,
"responsive": true,
"language": {
"zeroRecords": " "
},
});
});
// Populate Available Dynamic Analyzer instances.
{% for i in wifi_devices %}
$('#ios_dynamic').append($('<option>',{
value: '{{ i.0 }}:{{ i.1 }}',
text: '{{ i.0 }}:{{ i.1 }} (WiFi)',
}));
{% endfor %}
{% for i in usb_devices %}
$('#ios_dynamic').append($('<option>',{
value: '{{ i.id}}',
text: '{{ i.model }} - iOS {{ i.version}} - {{ i.serial}}',
}));
{% endfor %}
var choosen_device = document.querySelector('#choosen_device').value;
if (choosen_device){
$('#ios_dynamic option').each(function(){
if ($(this).val() === choosen_device){
$(this).prop('selected', true);
}
});
} else {
// Add text select a device
$('#ios_dynamic').append($('<option>',{
value: '',
text: 'Select a Jailbroken iOS Device',
selected: true,
}));
}
// List apps onload
if ($('#ios_dynamic').val()){
list_apps();
}
// List apps on device change
$('#ios_dynamic').on('change', function() {
location.href = '{% url 'dynamic_ios_device' %}?device_id=' + $(this).val();
});
// List installed apps
function list_apps(){
$('#in_device tbody tr').remove();
$('#in_device thead').remove();
dynamic_loader();
$.ajax({
url: '{% url 'get_ios_device' %}',
type : 'POST',
data : {
csrfmiddlewaretoken: '{{ csrf_token }}',
device_id: $('#ios_dynamic').val(),
},
dataType: 'json',
success : function(json) {
if(!json)
stop_loader();
if(!json.status){
stop_loader();
Swal.fire(
'Listing apps failed',
'Device is not ready',
'error'
)
}
if (json.status==='ok'){
$('#in_device').append('<thead><tr> <th>APP</th> <th>BUNDLE ID</th> <th>APP TYPE</th> <th>ACTION</th> </tr></thead><tbody></tbody>');
for(var i=0; i < json.apps.length; i++) {
let name = json.apps[i].app_name || 'Unknown App';
let bundle = json.apps[i].bundle_id || 'Unknown Bundle';
let type = json.apps[i].app_type || 'Unknown';
let reportExists = true;
let iconb64;
if (json.apps[i].app_icon)
iconb64 = 'data:image/png;base64,' + json.apps[i].app_icon;
else
iconb64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
var buttonState = 'disabled';
if (reportExists)
buttonState = '';
var order;
if (type === 'User')
order = 'first';
else
order = 'last';
var url = `{% url 'dynamic_analyzer_device' %}?bundle_id=${escapeHtml(bundle)}&device_id=${$('#ios_dynamic').val()}`;
$('#in_device > tbody:' + order + '-child').append(
`<tr><td align="center">
<img id="app_icon" src="${escapeHtml(iconb64)}"/><br/>
<strong>${escapeHtml(name)}</strong></td>
<td>${escapeHtml(bundle)}</td>
<td>${escapeHtml(type)}</td>
<td><p>
<a class="btn btn-success disable" onclick="dynamic_loader()" href="${url}"><i class="fab fa-apple"></i> Start Dynamic Analysis</a>
<a class="btn btn-info ${buttonState}" onclick="dynamic_loader()" href="../../ios/view_report_device/${escapeHtml(bundle)}"><i class="fa fa-mobile"></i> View Report </a>
</p>
</td></tr>`);
}
$('.upload-install-btn').removeClass('disabled');
stop_loader();
}
else {
stop_loader();
Swal.fire(
'Listing apps failed',
json.message,
'error'
)
}
},
error : function(xhr,errmsg,err) {
stop_loader();
console.log(errmsg)
}
});
}
// Upload and Install IPA to device
function upload_and_install(checksum){
dynamic_loader();
$.ajax({
url: '{% url 'install_ipa_device' %}',
type : 'POST',
dataType: 'json',
data : {
csrfmiddlewaretoken: '{{ csrf_token }}',
device_id: $('#ios_dynamic').val(),
checksum: checksum,
},
success : function(json) {
if (json.status !== 'ok'){
stop_loader();
Swal.fire(
'IPA Install Failed',
'Failed to Install IPA: ' + json.message,
'error'
)
} else {
stop_loader();
Swal.fire(
'IPA Installed',
'The IPA is successfully installed',
'success'
);
list_apps();
}
},
error : function(xhr,errmsg,err) {
stop_loader();
Swal.fire(
'IPA Install Errored',
errmsg,
'error'
)
}
});
}
</script>
{% endblock %}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
{% extends "base/base_layout.html" %}
{% block sidebar_option %}
sidebar-collapse
{% endblock %}
{% block content %}
<style>
#logs {
font-family: monospace;
white-space: pre-wrap;
background-color: #1e1e1e;
color: #eee;
padding: 1em;
}
</style>
<div class="content-wrapper">
<div class="content-header">
</div>
<div class="container-fluid">
<div class="row">
<div class="col-lg-12">
<div class="card">
<div class="card-body">
<data id="pid" value="{{ pid }}"></data>
<h3 class="page-header"><strong>{% if pid %}{{pid}} Logs{% else %}System Logs{% endif %} </strong></h3>
<div class="row mb-3">
<div class="col-md-4">
<div class="d-flex align-items-center">
<label for="pid_select" class="mb-0 mr-2">Select Process:</label>
<select id="pid_select" class="form-control">
<option value="">All System Logs</option>
</select>
</div>
</div>
<div class="col-md-8">
<div id="messages">Data refreshed in every 10 seconds. <button id="clearLogs" class="btn btn-sm btn-secondary">Clear Logs</button></div>
</div>
</div>
<pre id="logs" style="max-height: 600px; overflow-y: auto; white-space: pre-wrap;"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script type="text/javascript">
function ansiToHtml(input) {
// First escape HTML characters to prevent injection
let result = input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Handle ANSI escape sequences
// Reset all formatting
result = result.replace(/\[0m/g, '</span>');
// Bold text
result = result.replace(/\[1m/g, '<span style="font-weight:bold">');
// Foreground colors (standard)
result = result.replace(/\[30m/g, '<span style="color:black">');
result = result.replace(/\[31m/g, '<span style="color:red">');
result = result.replace(/\[32m/g, '<span style="color:green">');
result = result.replace(/\[33m/g, '<span style="color:yellow">');
result = result.replace(/\[34m/g, '<span style="color:blue">');
result = result.replace(/\[35m/g, '<span style="color:magenta">');
result = result.replace(/\[36m/g, '<span style="color:cyan">');
result = result.replace(/\[37m/g, '<span style="color:white">');
// Foreground colors (bright)
result = result.replace(/\[90m/g, '<span style="color:gray">');
result = result.replace(/\[91m/g, '<span style="color:lightcoral">');
result = result.replace(/\[92m/g, '<span style="color:lightgreen">');
result = result.replace(/\[93m/g, '<span style="color:lightyellow">');
result = result.replace(/\[94m/g, '<span style="color:lightblue">');
result = result.replace(/\[95m/g, '<span style="color:violet">');
result = result.replace(/\[96m/g, '<span style="color:lightcyan">');
result = result.replace(/\[97m/g, '<span style="color:white">');
// Background colors
result = result.replace(/\[40m/g, '<span style="background-color:black">');
result = result.replace(/\[41m/g, '<span style="background-color:red">');
result = result.replace(/\[42m/g, '<span style="background-color:green">');
result = result.replace(/\[43m/g, '<span style="background-color:yellow">');
result = result.replace(/\[44m/g, '<span style="background-color:blue">');
result = result.replace(/\[45m/g, '<span style="background-color:magenta">');
result = result.replace(/\[46m/g, '<span style="background-color:cyan">');
result = result.replace(/\[47m/g, '<span style="background-color:white">');
// Background colors (bright)
result = result.replace(/\[100m/g, '<span style="background-color:gray">');
result = result.replace(/\[101m/g, '<span style="background-color:lightcoral">');
result = result.replace(/\[102m/g, '<span style="background-color:lightgreen">');
result = result.replace(/\[103m/g, '<span style="background-color:lightyellow">');
result = result.replace(/\[104m/g, '<span style="background-color:lightblue">');
result = result.replace(/\[105m/g, '<span style="background-color:violet">');
result = result.replace(/\[106m/g, '<span style="background-color:lightcyan">');
result = result.replace(/\[107m/g, '<span style="background-color:white">');
// Handle combined codes like [1;36m (bold + cyan)
result = result.replace(/\[1;30m/g, '<span style="font-weight:bold;color:black">');
result = result.replace(/\[1;31m/g, '<span style="font-weight:bold;color:red">');
result = result.replace(/\[1;32m/g, '<span style="font-weight:bold;color:green">');
result = result.replace(/\[1;33m/g, '<span style="font-weight:bold;color:yellow">');
result = result.replace(/\[1;34m/g, '<span style="font-weight:bold;color:blue">');
result = result.replace(/\[1;35m/g, '<span style="font-weight:bold;color:magenta">');
result = result.replace(/\[1;36m/g, '<span style="font-weight:bold;color:cyan">');
result = result.replace(/\[1;37m/g, '<span style="font-weight:bold;color:white">');
// Handle other combined codes
result = result.replace(/\[1;90m/g, '<span style="font-weight:bold;color:gray">');
result = result.replace(/\[1;91m/g, '<span style="font-weight:bold;color:lightcoral">');
result = result.replace(/\[1;92m/g, '<span style="font-weight:bold;color:lightgreen">');
result = result.replace(/\[1;93m/g, '<span style="font-weight:bold;color:lightyellow">');
result = result.replace(/\[1;94m/g, '<span style="font-weight:bold;color:lightblue">');
result = result.replace(/\[1;95m/g, '<span style="font-weight:bold;color:violet">');
result = result.replace(/\[1;96m/g, '<span style="font-weight:bold;color:lightcyan">');
result = result.replace(/\[1;97m/g, '<span style="font-weight:bold;color:white">');
return result;
}
$(document).ready( function () {
// HTML escape function
function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Clear logs button functionality
$('#clearLogs').click(function() {
$('#logs').empty();
});
// Load process list on page load
loadProcessList();
// Handle PID selection change
$('#pid_select').change(function() {
var selectedPid = $(this).val();
var label = $(this).find('option:selected').text();
if (selectedPid) {
$('#pid').val(selectedPid);
$('h3 strong').text(label);
} else {
$('#pid').val('');
$('h3 strong').text('System Logs');
}
// Clear logs when switching processes
$('#logs').empty();
});
function loadProcessList() {
var data = {
'device_id': '{{ device_id }}',
'csrfmiddlewaretoken': '{{ csrf_token }}'
};
$.ajax({
url: '{% url "ps_device" %}',
type: 'POST',
dataType: 'json',
data: data,
success: function(json) {
if (json.status === 'ok' && json.message && Array.isArray(json.message)) {
var select = $('#pid_select');
// Keep the "All System Logs" option
select.find('option:not(:first)').remove();
json.message.forEach(function(process) {
var option = $('<option></option>')
.val(process.pid)
.text(process.pid + ': ' + escapeHtml(process.process_name));
select.append(option);
});
}
},
error: function(xhr, ajaxOptions, thrownError) {
console.log('Failed to load process list:', xhr.responseText);
}
});
}
function get_logs(){
var data = {
'device_id': '{{ device_id }}',
'csrfmiddlewaretoken': '{{ csrf_token }}'
};
var pid = document.getElementById('pid').value;
// check if pid is a digit , conmvert to int
if (pid && !isNaN(pid)) {
pid = parseInt(pid);
url = '{% url "system_logs_device" %}?device_id={{ device_id }}&pid=' + pid;
}
else {
url = '{% url "system_logs_device" %}?device_id={{ device_id }}';
}
$.ajax({
url : url,
type : "POST",
dataType: "json",
data : data,
success : function(json){
if (json.message && json.message.trim()) {
// Append new logs with timestamp
var timestamp = new Date().toLocaleTimeString();
var newLogEntry = '[' + timestamp + '] ' + json.message + '\n';
$('#logs').append(ansiToHtml(newLogEntry));
// Auto-scroll to bottom after any content changes
setTimeout(function() {
var logsElement = document.getElementById('logs');
if (logsElement) {
logsElement.scrollTop = logsElement.scrollHeight;
}
}, 10);
}
},
error : function(xhr, ajaxOptions, thrownError) {
console.log(xhr.responseText);
}
});
}
get_logs();
setInterval( function () {
get_logs();
}, 10000 );
});
</script>
{% endblock %}
@@ -393,7 +393,7 @@ function list_apps(){
else
order = 'last';
var url = `{% url 'dynamic_analyzer_ios'%}?bundleid=${escapeHtml(bundle)}&instance_id=${$('#ios_dynamic').val()}`;
var url = `{% url 'dynamic_analyzer_ios'%}?bundle_id=${escapeHtml(bundle)}&instance_id=${$('#ios_dynamic').val()}`;
$('#in_device > tbody:' + order + '-child').append(
`<tr><td align="center">
<img id="app_icon" src="data:image/png;base64,${escapeHtml(iconb64)}"/><br/>
@@ -13,7 +13,7 @@
<link rel="stylesheet" href="{% static "others/css/spinner.css" %}">
<link href="{% static "adminlte/plugins/sweetalert2/sweetalert2.min.css" %}" rel="stylesheet">
<style>
textarea {
#stat {
width: 100%;
height: 170px;
-moz-border-bottom-colors: none;
@@ -34,27 +34,43 @@
padding: 5px 8px;
transition: background-color 0.2s ease 0s;
}
textarea:focus {
#stat:focus {
background: none repeat scroll 0 0 #FFFFFF;
outline-width: 0;
}
.highlight { border: solid 2px #1e6dff;}
/* Complete isolation for CodeMirror */
#codemirror-isolation-container {
zoom: 1 !important;
transform: scale(1.25) !important; /* Counter the 0.8 zoom */
transform-origin: top left !important;
width: 80% !important; /* Adjust for scale */
height: 80% !important; /* Adjust for scale */
position: relative !important;
overflow: hidden !important;
#code-editor{
transform: scale(1.25) !important;
transform-origin: top left;
width: 80%;
height: auto;
overflow: auto;
max-height: 600px;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
background: #f8f9fa;
margin-bottom: 150px;
position: relative;
z-index: 1;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* Internet Explorer 10+ */
}
#code-editor::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
.CodeMirror {
height: 610px;
font-size: 11px;
border: 1px solid #eee;
}
.form-group {
position: relative;
z-index: 2;
margin-top: 20px;
}
.frida-pre {
font-family: "courier new", courier, monospace;
font-size: 11px;
@@ -97,6 +113,7 @@
<a href="{% url 'ios_live_pcap_download' %}?instance_id={{instance_id}}" id="pcap" class="btn btn-primary" role="button"><i class="fas fa-globe"></i> Download Live Packet Capture</a>
<a href="#" id="ss" class="btn btn-primary" role="button"><i class="fa fa-camera"></i> Take a Screenshot</a>
<a href="{% url 'ios_system_logs' %}?instance_id={{instance_id}}" target="_blank" id="system_logs" class="btn btn-primary"><i class="fa fa-water"></i> System Logs</a>
<a href="{% url 'ios_api_monitor' %}?checksum={{ checksum }}&bundle_id={{ bundle_id }}" id="apimon" target="_blank" class="btn btn-primary" style='display: none'><i class="fa fa-search"></i> Live API Monitor</a>
<a href="#" id="report" class="btn btn-info disabled" role="button"><i class="fa fa-file-alt"></i> Generate Report</a>
</div>
</div>
@@ -183,6 +200,9 @@
Jailbreak Detection Bypass
</label>
<label>
<input name="default_hooks" type="checkbox" value="ssl_pinning" checked>
SSL Pinning Bypass
</label>
</div>
<hr/>
<div class="form-group">
@@ -325,27 +345,20 @@
</div>
<!-- /.card-header -->
<div class="card-body">
<div class="row">
<div class="col-12" id="code-editor">
<div id="codemirror-isolation-container">
<div id="code-editor">
<textarea id="code-js" rows="100">
// Use send() for logging
</textarea>
</div>
</div>
<div class="col-12">
<br/>
<div class="form-group">
<span class="box-title">Available Scripts (Use CTRL to choose multiple) </span>
<button id="loadscript" type="submit" class="btn btn-primary btn-sm">Load</button>
</div>
<div class="form-group">
<select id="fd_scs" size="10" multiple="" class="form-control">
</select>
</div>
</div>
</div>
<!-- end row-->
<br/>
<div class="form-group">
<span class="box-title">Available Scripts (Use CTRL to choose multiple) </span>
<button id="loadscript" type="submit" class="btn btn-primary btn-sm">Load</button>
</div>
<div class="form-group">
<select id="fd_scs" size="10" multiple="" class="form-control">
</select>
</div>
</div>
<!-- /.card-body -->
@@ -476,12 +489,10 @@
var appContainerPath;
// Loading Spinner
function dynamic_loader(){
document.getElementById("code-editor").style.visibility = "hidden";
var loadingOverlay = document.querySelector('.loading');
loadingOverlay.classList.remove('hidden');
}
function stop_loader(){
document.getElementById("code-editor").style.visibility = "visible";
var loadingOverlay = document.querySelector('.loading');
loadingOverlay.classList.add('hidden');
}
@@ -550,7 +561,7 @@ function load_frida_others(){
action('{% url 'list_frida_scripts' %}', {device: 'ios'}, function(json) {
if (json.status == 'ok'){
json.files.forEach(function(script) {
$('#fd_scs').append('<option value=' + escape(script) + '>' + escape(script) + '</option>');
$('<option>').val(script).text(script).appendTo('#fd_scs');
});
}
});
@@ -634,7 +645,9 @@ var Terminal = Terminal || function (cmdLineContainer, outputContainer) {
if (e.keyCode == 38 || e.keyCode == 40) {
this.value = history_[histpos_] ? history_[histpos_] : histtemp_;
this.value = this.value; // Sets cursor to end of input.
setTimeout(() => {
this.setSelectionRange(this.value.length, this.value.length);
}, 0); // Sets cursor to end of input.
}
}
}
@@ -651,11 +664,41 @@ var Terminal = Terminal || function (cmdLineContainer, outputContainer) {
}
function scrollToShellBottom(){
// Scroll to shell bottom
// Scroll to shell bottom and ensure shell is completely visible
var myElement = document.getElementById('shell');
// Scroll the shell content to the bottom
myElement.scrollTop = myElement.scrollHeight;
myElement.scrollIntoView({ block: 'end' });
// Ensure the shell element is visible in the viewport
myElement.scrollIntoView({
block: 'end',
behavior: 'smooth',
inline: 'nearest'
});
// For dynamic content, we need to continuously monitor and scroll
// Set up a MutationObserver to watch for content changes
if (!window.shellObserver) {
window.shellObserver = new MutationObserver(function(mutations) {
// When content changes, scroll to keep shell visible
myElement.scrollTop = myElement.scrollHeight;
myElement.scrollIntoView({
block: 'end',
behavior: 'smooth',
inline: 'nearest'
});
});
// Start observing the shell for content changes
window.shellObserver.observe(myElement, {
childList: true,
subtree: true,
characterData: true
});
}
}
var pwd = '/private/var/root';
var cd = '';
@@ -789,7 +832,7 @@ print_status("Environment is ready for Dynamic Analysis.");
// Frida Logs Tab
setInterval( function () {
$.ajax({
url : '{% url 'frida_logs' %}?hash={{ hash }}&stream=1',
url : '{% url 'frida_logs' %}?hash={{ checksum }}&stream=1',
type : "GET",
dataType: "json",
success : function(json){
@@ -817,7 +860,7 @@ function getContainerPath(){
// Screenshot
$("#ss").click(function() {
print_status("Taking a screenshot.");
action('{% url 'take_screenshot' %}', {instance_id: '{{ instance_id }}', save: '1', checksum: '{{ hash }}'}, function(json) {
action('{% url 'take_screenshot' %}', {instance_id: '{{ instance_id }}', save: '1', checksum: '{{ checksum }}'}, function(json) {
if (json.status==="ok"){
print_status("Screenshot taken!");
console.log(json.message)
@@ -1033,17 +1076,10 @@ $("#network").click(function() {
return false;
});
// Logcat Stream
$("#logstream").click(function() {
print_status("Logcat Steaming started.");
setTimeout(function(){ window.open(document.location.origin + '/logcat/?package=', '_blank');}, 500);
return false;
});
// App Run
$("#run").click(function() {
action('{% url 'run_app' %}', {instance_id: '{{ instance_id }}', bundle_id: '{{ bundle_id }}',}, function(json) {
action('{% url "run_app" %}', {instance_id: '{{ instance_id }}', bundle_id: '{{ bundle_id }}',}, function(json) {
if (json.status==="ok"){
print_status('Runnning application');
} else {
@@ -1090,6 +1126,10 @@ function frida_instrument(frida_action, pid=null, new_bundle_id=null){
$.each($("input[name='dump_hooks']:checked"), function(){
dump_hooks.push($(this).val());
});
if(dump_hooks.length > 0)
$('#apimon').show();
else
$('#apimon').hide();
var auxiliary_hooks = [];
$.each($("input[name='auxiliary']:checked"), function(){
auxiliary_hooks.push($(this).val());
@@ -1107,11 +1147,11 @@ function frida_instrument(frida_action, pid=null, new_bundle_id=null){
} else if (frida_action == 'get'){
print_status("Getting injected frida script.");
}
action('{% url 'ios_instrument' %}', {
action('{% url "ios_instrument" %}', {
frida_action,
pid, // attach pid
new_bundle_id, // attach bundle id
hash: '{{ hash }}',
hash: '{{ checksum }}',
bundle_id: '{{ bundle_id }}',
instance_id: '{{ instance_id}}',
default_hooks: default_hooks.join(","),
@@ -1132,8 +1172,7 @@ function frida_instrument(frida_action, pid=null, new_bundle_id=null){
if (frida_action == 'ps' && json.message && json.message.length > 0 && Array.isArray(json.message)){
$("#process_select").empty();
json.message.forEach(function(p) {
$('#process_select').append(
'<option value=' + escape(p.pid.toString()) + ',' + escape(p.identifier.toString()) + '>' + escape(p.pid.toString()) + ': ' + escapeHtml(p.name.toString()) + ' - ' + escape(p.identifier.toString()) + '</option>');
$('<option>').val(p.pid.toString() + ',' + p.identifier.toString()).text(p.pid.toString() + ': ' + p.name.toString() + ' - ' + p.identifier.toString()).appendTo('#process_select');
});
$('#frida_attach_mdl').modal('toggle');
} else if (frida_action =='get'){
@@ -298,6 +298,7 @@ pre {
<div align="center">
<p>
{% if frida_logs %}
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'ios_api_monitor' %}?checksum={{ hash }}&bundle_id={{ bundleid }}"><i class="fa fa-search"></i> API Monitor Logs</a>
<a target="_blank" class="btn btn-primary" role="button" href="{% url 'frida_logs' %}?hash={{ hash }}"><i class="fas fa-spider"></i> Frida Logs View</a>
{% endif %}
<a class="btn btn-success" target="_blank" role="button" href="{% url 'httptools' %}?project={{bundleid}}"><i class="fab fa-hire-a-helper"></i> Start HTTPTools</a>
@@ -457,10 +458,10 @@ pre {
{% for item in files %}
<tr>
<td>
<a href="javascript:cat_file('{{ item }}')">{{item}}</a>
<a href="#" onclick="cat_file('{{ item|escapejs }}'); return false;">{{item}}</a>
</td>
<td>
<a class="btn btn-info btn-sm" href="javascript:download_file('{{ item }}')"><i class="fa fa-solid fa-download"></i></a>
<a class="btn btn-info btn-sm" href="#" onclick="download_file('{{ item|escapejs }}'); return false;"><i class="fa fa-solid fa-download"></i></a>
</td>
</tr>
{% endfor %}
@@ -502,10 +503,10 @@ pre {
{% for item in datadir %}
<tr>
<td>
<a href="javascript:cat_file('{{ item.path }}')">{{ item.path | replace:"/private/var/mobile/Containers/Data/Application/|" }}</a>
<a href="#" onclick="cat_file('{{ item.path|escapejs }}'); return false;">{{ item.path | replace:"/private/var/mobile/Containers/Data/Application/|" }}</a>
</td>
<td>
<a class="btn btn-info btn-sm" href="javascript:download_file('{{ item.path }}')"><i class="fa fa-solid fa-download"></i></a>
<a class="btn btn-info btn-sm" href="#" onclick="download_file('{{ item.path|escapejs }}'); return false;"><i class="fa fa-solid fa-download"></i></a>
</td>
<td>
{{ item.fileProtectionKey }}
@@ -1423,7 +1424,8 @@ function action(url, data, on_success){
var code_title = document.getElementById('cat_title');
code_title.innerText = '';
code_block.innerText = '';
action('{% url 'ssh_execute' %}', {instance_id: '{{ instance_id }}', cmd: `cat -v "${file_name}"` }, function(json) {
var safe_name = file_name.replace(/'/g, "'\\''");
action('{% url "ssh_execute" %}', {instance_id: '{{ instance_id }}', cmd: "cat -v '" + safe_name + "'" }, function(json) {
if (json.status==="ok")
code_title.innerText = file_name;
code_block.innerText = json.message;
@@ -1431,32 +1433,53 @@ function action(url, data, on_success){
});
}
// Download file
function download_file(path){
// Download file using hidden iframe
function download_file(path){
// Create a hidden iframe for the download
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.name = 'download_iframe';
iframe.id = 'download_iframe_' + Date.now();
document.body.appendChild(iframe);
// Create the form
var form = document.createElement('form');
var element1 = document.createElement('input');
var element2 = document.createElement('input');
var element3 = document.createElement('input');
form.method = 'POST';
form.action = '{% url 'download_file' %}';
form.action = '{% url "download_file" %}';
form.target = iframe.name;
element1.value = '{{ instance_id }}';
element1.name = 'instance_id';
form.appendChild(element1);
// Create form elements
var instanceIdInput = document.createElement('input');
instanceIdInput.type = 'hidden';
instanceIdInput.name = 'instance_id';
instanceIdInput.value = '{{ instance_id }}';
form.appendChild(instanceIdInput);
var fileInput = document.createElement('input');
fileInput.type = 'hidden';
fileInput.name = 'file';
fileInput.value = path;
form.appendChild(fileInput);
element2.value = path;
element2.name = 'file';
form.appendChild(element2);
element3.value = '{{ csrf_token }}';
element3.name = 'csrf_token';
form.appendChild(element3);
var csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrfmiddlewaretoken';
csrfInput.value = '{{ csrf_token }}';
form.appendChild(csrfInput);
// Append form to body, submit, and clean up
document.body.appendChild(form);
form.submit();
form.remove();
// Clean up after a short delay
setTimeout(function() {
if (document.body.contains(form)) {
document.body.removeChild(form);
}
if (document.body.contains(iframe)) {
document.body.removeChild(iframe);
}
}, 2000);
}
</script>
{% endblock %}
@@ -31,7 +31,7 @@ $(document).ready( function () {
}
data.csrfmiddlewaretoken ='{{ csrf_token }}';
$.ajax({
url : '{% url 'ios_system_logs' %}',
url : '{% url "ios_system_logs" %}',
type : "POST",
dataType: "json",
data : data,
File diff suppressed because it is too large Load Diff
+13 -1
View File
@@ -32,7 +32,19 @@
<div class="card-body">
<h4 class="d-flex justify-content-center">iOS Dynamic Analyzer</h4>
<p class="card-text">Perform Dynamic Analysis of iOS Applications.</p>
<a href="{% url 'dynamic_ios' %}" class="btn btn-primary d-flex justify-content-center">iOS Dynamic Analyzer</a>
<div class="dropdown d-flex justify-content-center">
<button class="btn btn-primary dropdown-toggle" type="button" id="iosDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
iOS Dynamic Analyzer
</button>
<div class="dropdown-menu" aria-labelledby="iosDropdown">
<a class="dropdown-item" href="{% url 'dynamic_ios' %}">
<i class="fa fa-microchip mr-2"></i>Corellium VM
</a>
<a class="dropdown-item" href="{% url 'dynamic_ios_device' %}">
<i class="fas fa-mobile-alt mr-2"></i>Jailbroken Device
</a>
</div>
</div>
</div>
</div>
</div>
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mobsf"
version = "4.4.5"
version = "4.5.0"
description = "Mobile Security Framework (MobSF) is an automated, all-in-one mobile application (Android/iOS/Windows) pen-testing, malware analysis and security assessment framework capable of performing static and dynamic analysis."
keywords = ["mobsf", "mobile security framework", "mobile security", "security tool", "static analysis", "dynamic analysis", "malware analysis"]
authors = ["Ajin Abraham <[email protected]>"]