mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-11 22:50:09 +00:00
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:
co-authored by
CylentSec
Oz
parent
6f8a43c1b7
commit
6e875fb77b
@@ -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}}');
|
||||
|
||||
+64
-50
@@ -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);
|
||||
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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),
|
||||
])
|
||||
|
||||
@@ -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)
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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 & 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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
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
@@ -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
@@ -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]>"]
|
||||
|
||||
Reference in New Issue
Block a user