├── README.md ├── LICENSE └── ios ├── EnumerateClasses.js ├── EnumerateMethods.js ├── DetectURLs.js ├── DisplayUIAlert.js ├── HookBeforeAppDelegate.py ├── EnumerateLoadedModules.js ├── BypassLAContextAuthentication.js ├── DetectUIPasteboardObserver.js ├── DetectHiddenViews.js ├── RevealHiddenViews.js ├── InspectWebViewImplementations.js ├── EnumerateKeychainContents.js └── EnumerateFileDataProtectionClasses.js /README.md: -------------------------------------------------------------------------------- 1 | # frida-scripts 2 | A collection of Frida scripts that I created for iOS and Android mobile application assessments 3 | 4 | To use these scripts, ensure that frida is installed on your testing machine, and frida-server is running on the mobile device. Then use the following command to use the desired script: 5 | 6 | ``` 7 | $ frida -U -l [SCRIPT-NAME].js [PROCESS-NAME] 8 | ``` 9 | 10 | You can find the process name using ```frida-ps```: 11 | 12 | ``` 13 | $ frida-ps -Uai 14 | ``` 15 | 16 | Some scripts are best run from when the application first starts. In those cases, you can use early instrumentation as shown below: 17 | 18 | ``` 19 | $ frida -U -l [SCRIPT-NAME] --no-puase -f [APP-IDENTIFIER] 20 | ``` 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 KittyNighthawk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ios/EnumerateClasses.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: EnumerateClasses 3 | * 4 | * Description: This Frida script hooks all the classes of an application and prints their names to console. It produces a lot of 5 | * output as it does not remove standard iOS SDK classes. 6 | * 7 | * Created by Kitty Nighthawk 2020 8 | */ 9 | 10 | // Constant values for the ANSI colour codes 11 | const reset = "\x1b[0m"; 12 | const black = "\u001b[30m"; 13 | const red = "\u001b[31m"; 14 | const green = "\u001b[32m"; 15 | const yellow = "\u001b[33m"; 16 | const blue = "\u001b[34m"; 17 | const magenta = "\u001b[35m"; 18 | const cyan = "\u001b[36m"; 19 | const white = "\u001b[37m"; 20 | const bold = "\u001b[1m"; 21 | 22 | console.log(magenta, "[*] Starting enumerate classes script", reset); 23 | 24 | if(ObjC.available) { 25 | try { 26 | var classes = []; 27 | 28 | for (var class_name in ObjC.classes) { 29 | console.log(bold, green, "[+] Class found: ", class_name, reset); 30 | } 31 | } catch(err) { 32 | console.log(red, "[!] An exception occured: ", reset, err.message); 33 | } 34 | } else { 35 | console.log(red, "[-] Objective-C runtime is not available.", reset); 36 | } 37 | 38 | console.log(cyan, "[*] Enumerate classes script complete", reset); 39 | -------------------------------------------------------------------------------- /ios/EnumerateMethods.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: EnumerateMethods 3 | * 4 | * Description: This Frida script is used to hook a class and print out all the methods of 5 | * that class 6 | * 7 | * Created by Kitty Nighthawk 2020 8 | */ 9 | 10 | // Constant values for the ANSI colour codes 11 | const reset = "\x1b[0m"; 12 | const black = "\u001b[30m"; 13 | const red = "\u001b[31m"; 14 | const green = "\u001b[32m"; 15 | const yellow = "\u001b[33m"; 16 | const blue = "\u001b[34m"; 17 | const magenta = "\u001b[35m"; 18 | const cyan = "\u001b[36m"; 19 | const white = "\u001b[37m"; 20 | const bold = "\u001b[1m"; 21 | 22 | console.log(magenta, "[*] Started method enumeration script", reset); 23 | 24 | if (ObjC.available) { 25 | try { 26 | var className = "MyClassName"; 27 | var methods = eval('ObjC.classes.' + className + '.$methods'); 28 | 29 | for (var i=0; i -1)) { 40 | var createdObservers = []; 41 | var removedObservers = []; 42 | 43 | console.log(cyan, "[*] View Controller name:", reset, className); 44 | 45 | resolver.enumerateMatchesSync('*[__NSObserver observerWithCenter:queue:name:object:block:]', { 46 | onMatch: function(match) { 47 | createdObservers.push(match); 48 | }, 49 | onComplete: function() { 50 | // 51 | } 52 | }); 53 | 54 | resolver.enumerateMatchesSync('*[__NSObserver forgetObserver:]', { 55 | onMatch: function(match) { 56 | removedObservers.push(match); 57 | }, 58 | onComplete: function() { 59 | // 60 | } 61 | }); 62 | 63 | createdObservers.forEach(function(observer) { 64 | Interceptor.attach(observer.address, { 65 | onEnter: function (args) { 66 | // args[2] = observerWithCenter: 67 | // args[3] = queue: 68 | // args[4] = name: 69 | // args[5] = object: 70 | // args[6] = block: 71 | const observerName = ObjC.Object(ptr(args[4])).toString(); 72 | console.log(green, "[+] UIPasteboardChangedNotification observer registered (potential IPC entry point)", reset); 73 | } 74 | }); 75 | }); 76 | 77 | removedObservers.forEach(function(observer) { 78 | Interceptor.attach(observer.address, { 79 | onEnter: function (args) { 80 | // args[2] = forgetObserver: 81 | const timestamp = new Date(); 82 | const observerObj = new ObjC.Object(ptr(args[2])); 83 | console.log(yellow, "[?] UIPasteboardChangedNotification observer deregistered (interesting)", reset); 84 | } 85 | }); 86 | }); 87 | } 88 | }, 89 | }); 90 | } catch(err) { 91 | console.log(red, "[!] An exception occured: ", reset, err.message); 92 | } 93 | } else { 94 | console.log(red, "[-] Objective-C runtime is not available.", reset); 95 | } 96 | 97 | console.log(cyan, "[*] UIPasteboard Observers will now be reported", reset); 98 | -------------------------------------------------------------------------------- /ios/DetectHiddenViews.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: DetectHiddenViews 3 | * 4 | * Description: This Frida script is used to detect when a scene has changed, and then extract the UIWindow contents 5 | * and search through it for hidden views. 6 | * 7 | * The idea is that this script is run during application discovery. It will print out any hidden views it sees 8 | * as you traverse the application. 9 | * 10 | * This script works by hooking any calls to viewDidAppear: which is a function of View Controllers. In this case, we 11 | * only want to hook viewDidAppear functions whose class is not UINavigationController (the superclass) but of the 12 | * individual, unique subclassess (the View Controllers of each scene in the application). Once hooked, it can be 13 | * assumed that a new scene has been loaded, so we extract the contents of the UIWindow of that scene (this holds 14 | * all the other views within it) and then use a regular expression to identify any views with the hidden attribute 15 | * set to 'YES' 16 | * 17 | * Created by Kitty Nighthawk 2020 18 | */ 19 | 20 | // Constant values for the ANSI colour codes 21 | const reset = "\x1b[0m"; 22 | const black = "\u001b[30m"; 23 | const red = "\u001b[31m"; 24 | const green = "\u001b[32m"; 25 | const yellow = "\u001b[33m"; 26 | const blue = "\u001b[34m"; 27 | const magenta = "\u001b[35m"; 28 | const cyan = "\u001b[36m"; 29 | const white = "\u001b[37m"; 30 | const bold = "\u001b[1m"; 31 | 32 | console.log(magenta, "[*] Starting hidden view detector script.", reset); 33 | 34 | if(ObjC.available) { 35 | try { 36 | Interceptor.attach(ObjC.classes.UIViewController['- viewDidAppear:'].implementation, { 37 | onEnter: function (args) { 38 | /* args[0] is 'self' 39 | * args[1] is the selector (the funtion name) 40 | * args[2] is the first argument to the function 41 | */ 42 | 43 | // Need to detect viewDidLoad calls on non UIViewController classes. This will be the custom views 44 | var className = ObjC.Object(args[0]).$className; 45 | 46 | if(className != "UINavigationController") { 47 | // Print the View Controller's name 48 | console.log(cyan, "[*] View Controller name: ", reset, className); 49 | // Pull all the UIWindow information 50 | var windowData = ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString(); 51 | 52 | try { 53 | // Search for 'hidden = YES', then print out the line 54 | // keyWindow will output a multiline string, so need to use global 'g' and multiline 'm' regex flags 55 | var regex = /^.*(<.*hidden\s=\s[yes|YES].*>).*$/gm; 56 | var line = regex.exec(windowData); 57 | 58 | // As the string is multiline, this loops through it finding each match. 59 | // This while loop will run as long as a match is found (if nothing is found, then line will equal null) 60 | while(line != null) { 61 | // Print the current match 62 | // line[0] = whole match 63 | // line[1] = first group of match 64 | console.log(bold, green,"[+] Hidden view detected: ", reset, line[1]); 65 | // Now continue searching the string for any other matches 66 | line = regex.exec(windowData); 67 | } 68 | } catch(err) { 69 | console.log(red, "[!] An error occured whilst parsing: ", reset, err.message); 70 | } 71 | } 72 | }, 73 | onLeave: function (retval) { 74 | // Left blank for code consistency 75 | } 76 | }); 77 | } catch(err) { 78 | console.log(red, "[!] An exception occured: ", reset, err.message); 79 | } 80 | } else { 81 | console.log(red, "[-] Objective-C runtime is not available.", reset); 82 | } 83 | 84 | console.log(cyan, "[*] Detection of hidden views is now running", reset); 85 | -------------------------------------------------------------------------------- /ios/RevealHiddenViews.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: RevealHiddenViews 3 | * 4 | * Description: This Frida script is used to detect when a view has changed, and then extract the UIWindow contents 5 | * and search through it for hidden views. Any views identified as hidden will be altered to reveal them in the UI 6 | * 7 | * The idea is that this script is run during application discovery. It will identify any hidden views during 8 | * discovery and reveal them. 9 | * 10 | * This script works by hooking any calls to viewDidAppear: which is a function of View Controllers. In this case, we 11 | * only want to hook viewDidAppear functions whose class is not UINavigationController (the superclass) but of the 12 | * individual, unique subclassess (the View Controllers of each scene in the application). Once hooked, it can be 13 | * assumed that a new scene has been loaded, so we extract the contents of the UIWindow of that scene (this holds 14 | * all the other views within it) and then use a regular expression to identify any views with the hidden attribute 15 | * set to 'YES'. Once identified, the script then flips the hidden switch from YES to NO to reveal them. 16 | * 17 | * Created by Kitty Nighthawk 2020 18 | */ 19 | 20 | // Constant values for the ANSI colour codes 21 | const reset = "\x1b[0m"; 22 | const black = "\u001b[30m"; 23 | const red = "\u001b[31m"; 24 | const green = "\u001b[32m"; 25 | const yellow = "\u001b[33m"; 26 | const blue = "\u001b[34m"; 27 | const magenta = "\u001b[35m"; 28 | const cyan = "\u001b[36m"; 29 | const white = "\u001b[37m"; 30 | const bold = "\u001b[1m"; 31 | 32 | console.log(magenta, "[*] Starting hidden view revealer script.", reset); 33 | 34 | if(ObjC.available) { 35 | try { 36 | Interceptor.attach(ObjC.classes.UIViewController['- viewDidAppear:'].implementation, { 37 | onEnter: function (args) { 38 | /* args[0] is 'self' 39 | * args[1] is the selector (the funtion name) 40 | * args[2] is the first argument to the function 41 | */ 42 | 43 | // Need to detect viewDidLoad calls on non UIViewController classes. This will be the custom views 44 | var className = ObjC.Object(args[0]).$className; 45 | 46 | if(className != "UINavigationController") { 47 | // Print the View Controller's name 48 | console.log(cyan, "[*] View Controller name: ", reset, className); 49 | // Pull all the UIWindow information 50 | var windowData = ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString(); 51 | 52 | try { 53 | // Search for 'hidden = YES', then print out the line 54 | // keyWindow will output a multiline string, so need to use global 'g' and multiline 'm' regex flags 55 | var regex = /^.*(<.*hidden\s=\s[yes|YES].*>).*$/gm; 56 | var line = regex.exec(windowData); 57 | 58 | // As the string is multiline, this loops through it finding each match. 59 | // This while loop will run as long as a match is found (if nothing is found, then line will equal null) 60 | while(line != null) { 61 | // Print the current match 62 | // line[0] = whole match 63 | // line[1] = first group of match 64 | console.log(green,"[*] Hidden view detected: ", reset, line[1]); 65 | // Now make the hidden=YES attribute become hidden=NO 66 | try { 67 | // First, get the address of the view 68 | var regex2 = /^.*:\s(0x.*)>>$/gm; 69 | var line2 = regex2.exec(line) 70 | 71 | // Assuming we got a match in group 1... 72 | if(line2[1] != null) { 73 | // Then, make a NativePointer of the view from the address 74 | var ptrToObject = new NativePointer(line2[1]); 75 | // Now, create a new variable which points to the Objective-C object at the pointer 76 | var hiddenViewObj = ObjC.Object(ptrToObject); 77 | // Now, change the isHidden property to false with .setHidden_(false) 78 | hiddenViewObj.setHidden_(false); 79 | // Print message that the view has been unhidden 80 | console.log(bold, green, "[+] Revealed hidden view: ", reset, line[1]); 81 | } 82 | } catch(err) { 83 | console.log(red, "[!] Exception occured when trying to modify hidden attribute: ", reset, err.message); 84 | } 85 | 86 | // Now continue searching the string for any other matches 87 | line = regex.exec(windowData); 88 | } 89 | } catch(err) { 90 | console.log(red, "[!] An error occured whilst parsing: ", reset, err.message); 91 | } 92 | } 93 | }, 94 | onLeave: function (retval) { 95 | // Left blank for code consistency 96 | } 97 | }); 98 | } catch(err) { 99 | console.log(red, "[!] An exception occured: ", reset, err.message); 100 | } 101 | } else { 102 | console.log(red, "[-] Objective-C runtime is not available.", reset); 103 | } 104 | 105 | console.log(cyan, "[*] Hidden views are now being revealed", reset); 106 | -------------------------------------------------------------------------------- /ios/InspectWebViewImplementations.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Title: InspectWebViewImplementations 3 | * 4 | * Description: This Frida script will hook into any implementations of WebViews (UIWebView or WKWebView) and print 5 | their properties to the console, highlighting any that are insecure/may present vulnerabilities 6 | * 7 | * Created by Kitty Nighthawk 2020 8 | */ 9 | 10 | // Constant values for the ANSI colour codes 11 | const reset = "\x1b[0m"; 12 | const black = "\u001b[30m"; 13 | const red = "\u001b[31m"; 14 | const green = "\u001b[32m"; 15 | const yellow = "\u001b[33m"; 16 | const blue = "\u001b[34m"; 17 | const magenta = "\u001b[35m"; 18 | const cyan = "\u001b[36m"; 19 | const white = "\u001b[37m"; 20 | const bold = "\u001b[1m"; 21 | 22 | console.log(magenta, "[*] Starting WebView inspector script", reset); 23 | 24 | if(ObjC.available) { 25 | try { 26 | var resolver = new ApiResolver('objc'); 27 | var className = ""; 28 | var uiWebViewObjectPtrs = []; 29 | var wkWebViewObjectPtrs = []; 30 | var uiWebViewlastAccessedURL = ""; 31 | var wkAccessFilesFromFileURLs = false; 32 | 33 | // This hook handles detecting WebView views as they appear in the currently displayed window 34 | Interceptor.attach(ObjC.classes.UIViewController['- viewDidAppear:'].implementation, { 35 | onEnter: function(args) { 36 | // Get the ViewController name 37 | className = new ObjC.Object(args[0]).$className; 38 | if(className != "UINavigationController" && className != "UICompatibilityInputViewController" && className != "UIAlertController") { 39 | console.log("\nView Controller Name: ", cyan, className, reset); 40 | } 41 | 42 | // Now, get the views details 43 | var keyWindowDesc = ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString(); 44 | // Find any instances of WKWebView and UIWebView, grab the first argument (the pointer to the object in memory) 45 | var uiRegexp = /^.*<(UIWebView|WKWebView):\s(0x[0-9a-f]*)/gm; 46 | var regexpMatch = uiRegexp.exec(keyWindowDesc); 47 | // GROUP 0: Whole matching line 48 | // GROUP 1: UIWebView || WKWebView 49 | // GROUP 2: POINTER 50 | 51 | uiWebViewObjectPtrs = []; 52 | wkWebViewObjectPtrs = []; 53 | 54 | while(regexpMatch != null) { 55 | if(regexpMatch[1] == "UIWebView") { 56 | uiWebViewObjectPtrs.push(regexpMatch[2]); 57 | } else if(regexpMatch[1] == "WKWebView") { 58 | wkWebViewObjectPtrs.push(regexpMatch[2]); 59 | } else { 60 | console.log("[DEBUG] Non-standard webview?"); 61 | } 62 | // Multiline string being parsed, so get the next line and continue until there are no more lines (null) 63 | regexpMatch = uiRegexp.exec(keyWindowDesc); 64 | } 65 | 66 | // Now, use those matches to hook UIWebView load commands to find out what the URL requested is 67 | // Needs to be done on initial entry or we may miss the load command 68 | if(uiWebViewObjectPtrs.length > 0) { 69 | uiWebViewObjectPtrs.forEach(function(webviewPtr) { 70 | var webview = new ObjC.Object(ptr(webviewPtr)); 71 | var NSURLRequest = new ObjC.Object(ptr(webview.request())); 72 | var url = NSURLRequest.URL(); 73 | uiWebViewlastAccessedURL = url; 74 | }); 75 | } 76 | }, 77 | onLeave: function() { 78 | if(className != "UINavigationController" && className != "UICompatibilityInputViewController") { 79 | 80 | uiWebViewObjectPtrs.forEach(function(ptrStr) { 81 | var UIWebView = new ObjC.Object(ptr(ptrStr)); 82 | console.log(red, "[+]", reset, "UIWebView detected", reset); 83 | console.log(cyan, "[+]", reset, "Title: n/a"); 84 | console.log(cyan, "[+]", reset, "URL:", uiWebViewlastAccessedURL); 85 | console.log(red, "[+]", reset, "JavaScript: Enabled"); 86 | console.log(red, "[+]", reset, "JavaScriptCanOpenWindowsAutomatically: Enabled"); 87 | console.log(red, "[+]", reset, "Mixed Content: Enabled"); 88 | console.log(red, "[+]", reset, "Allow File Access: Enabled"); 89 | console.log(red, "[+]", reset, "Allow File Access From File URLs: Enabled"); 90 | console.log(red, "[+]", reset, "Allow Universal Access from Files (Same-Origin Policy Ignored): Enabled\n"); 91 | }); 92 | 93 | wkWebViewObjectPtrs.forEach(function(ptrStr) { 94 | var WKWebView = new ObjC.Object(ptr(ptrStr)); 95 | var WKWebViewConfiguration = new ObjC.Object(ptr(WKWebView.configuration())); // Creating a new object from the configuration pointer results in EXC_BAD_ACCESS 96 | var WKPreferences = new ObjC.Object(ptr(WKWebViewConfiguration.preferences())); 97 | var WKUserContentController = new ObjC.Object(ptr(WKWebViewConfiguration.userContentController())); 98 | 99 | var javaScriptEnabled = WKPreferences.javaScriptEnabled(); 100 | var hasOnlySecureContent = WKWebView.hasOnlySecureContent(); 101 | var javaScriptCanOpenWindowsAutomatically = WKPreferences.javaScriptCanOpenWindowsAutomatically(); 102 | var title = WKWebView.title(); 103 | 104 | console.log(green, "[+]", reset, "WKWebView detected"); 105 | if(title != "") { 106 | console.log(cyan, "[+]", reset, "Title:", title); 107 | } else { 108 | console.log(cyan, "[+]", reset, "Title: No title set"); 109 | } 110 | console.log(cyan, "[+]", reset, "URL:", WKWebView.URL()); 111 | if(javaScriptEnabled) { 112 | console.log(yellow, "[+]", reset, "JavaScript: Enabled"); 113 | } else { 114 | console.log(green, "[+]", reset, "JavaScript: Disabled"); 115 | } 116 | if(javaScriptCanOpenWindowsAutomatically) { 117 | console.log(red, "[+]", reset, "JavaScript Can Open Windows Automatically: Enabled"); 118 | } else { 119 | console.log(green, "[+]", reset, "JavaScript Can Open Windows Automatically: Disabled"); 120 | } 121 | if(hasOnlySecureContent) { 122 | console.log(green, "[+]", reset, "Mixed Content: Disabled"); 123 | } else { 124 | console.log(red, "[+]", reset, "Mixed Content: Enabled"); 125 | } 126 | console.log(yellow, "[+]", reset, "Access to Local Files: Enabled"); 127 | if(wkAccessFilesFromFileURLs) { 128 | console.log(red, "[+]", reset, "Access to Files from Files: Enabled"); 129 | } else { 130 | console.log(green, "[+]", reset, "Access to Files from Files: Disabled"); 131 | } 132 | wkAccessFilesFromFileURLs = false; 133 | console.log(green, "[+]", reset, "Universal Access from Files (Same-Origin Policy Ignored): Disabled\n"); 134 | 135 | }); 136 | } 137 | }, 138 | }); 139 | 140 | // This hook handles detecting any dynamically generated JavaScript that is evaluated within a WebView 141 | var uiWebViewJSEvals = resolver.enumerateMatchesSync('-[UIWebView stringByEvaluatingJavaScriptFromString:]'); 142 | var wkWebViewJSEvals = resolver.enumerateMatchesSync('-[WKWebView evaluateJavaScript:completionHandler:]'); 143 | 144 | uiWebViewJSEvals.forEach(function(instance) { 145 | Interceptor.attach(instance.address, { 146 | onEnter: function(args) { 147 | console.log(red, "[UIWebView] Dynamic JavaScript Evaluation in WebView Detected", reset); 148 | console.log("\tString evaluated as JavaScript: ",yellow , ObjC.Object(ptr(args[2])).toString(), reset); 149 | }, 150 | }); 151 | }); 152 | 153 | wkWebViewJSEvals.forEach(function(instance) { 154 | Interceptor.attach(instance.address, { 155 | onEnter: function(args) { 156 | console.log(red, "[WKWebView] Dynamic JavaScript Evaluation in WebView Detected", reset); 157 | console.log("\tString evaluated as JavaScript: ",yellow , ObjC.Object(ptr(args[2])).toString(), reset); 158 | }, 159 | }); 160 | }); 161 | 162 | // This hook detects any WKScriptMessageHandlers being registered to the WebView. These can indicate the presence 163 | // of a JavaScript bridge 164 | var wkWebViewJSHandlers = resolver.enumerateMatchesSync('-[WKUserContentController addScriptMessageHandler:name:]'); 165 | 166 | wkWebViewJSHandlers.forEach(function(instance) { 167 | Interceptor.attach(instance.address, { 168 | onEnter: function(args) { 169 | console.log(yellow, "[WKWebView] Message Handler Registered (Could be a JavaScript bridge)", reset) 170 | console.log("\tHandler name: ", cyan, ObjC.Object(ptr(args[3])).toString(), reset); 171 | }, 172 | }); 173 | }); 174 | 175 | // This hook detects if a WKWebView is explicitly allowing files access from files. This is a private API 176 | // so shouldn't pass App Store Review 177 | var wkWebViewPrefsSetters = resolver.enumerateMatchesSync('-[WKPreferences _setAllowFileAccessFromFileURLs:]'); 178 | 179 | wkWebViewPrefsSetters.forEach(function(instance) { 180 | Interceptor.attach(instance.address, { 181 | onEnter: function(args) { 182 | if(args[2] != 0) { 183 | console.log(red, "[WKWebView] Allow file access from file URLs explicitly allowed", reset); 184 | wkAccessFilesFromFileURLs = true; 185 | } else { 186 | console.log(yellow, "[WKWebView] Suspicious modification of allow file access from file URLs key", reset); 187 | } 188 | }, 189 | }); 190 | }); 191 | 192 | // This hook detects UIWebView loadHTMLString(_:baseURL:) calls where baseURL is null, which means the WebView will 193 | // accept applewebdata:// which allows for local file retreival 194 | var uiWebViewLoadHTMLStrings = resolver.enumerateMatchesSync('-[UIWebView loadHTMLString:baseURL:]'); 195 | 196 | uiWebViewLoadHTMLStrings.forEach(function(instance) { 197 | Interceptor.attach(instance.address, { 198 | onEnter: function(args) { 199 | var value = ObjC.Object(ptr(args[3])).toString(); 200 | if(value == "nil") { 201 | console.log(red, "[UIWebView] HTML file loaded with nil baseURL value (check for applewebdata://)", reset); 202 | } 203 | }, 204 | }); 205 | }); 206 | 207 | } catch(err) { 208 | console.log(red, "[!] An exception occured: ", reset, err.message); 209 | } 210 | } else { 211 | console.log(red, "[-] Objective-C runtime is not available.", reset); 212 | } 213 | 214 | console.log(cyan, "[*] WebView implementations are now being inspected", reset); 215 | -------------------------------------------------------------------------------- /ios/EnumerateKeychainContents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Title: EnumerateKeychainContents 3 | * 4 | * Description: This Frida script enumerates all the keys added to the Keychain by the application it injects into. Note 5 | * that this means it does not extract the entirety of the Keychain. It is capable of extracting a list of keys including 6 | * their attributes, creating a CSV of the contents for easy review, and performing fitering to only identify those 7 | * keys with weak properties. 8 | * 9 | * Created by KittyNighthawk (2021) 10 | */ 11 | 12 | //MARK: - ANSII Colours 13 | const RESET = "\x1b[0m"; 14 | const BLACK = "\u001b[30m"; 15 | const RED = "\u001b[31m"; 16 | const GREEN = "\u001b[32m"; 17 | const YELLOW = "\u001b[33m"; 18 | const BLUE = "\u001b[34m"; 19 | const MAGENTA = "\u001b[35m"; 20 | const CYAN = "\u001b[36m"; 21 | const WHITE = "\u001b[37m"; 22 | const BOLD = "\u001b[1m"; 23 | 24 | //MARK: - Options 25 | // When enabled, this will filter out keys which have whenPasscodeSetThisDeviceOnly as the accessibility setting 26 | const FILTER_ACCESSIBILITY_CLASS = false; 27 | // This will output the results as a list on the CLI 28 | const OUTPUT_AS_LIST = true; 29 | // This will output a CSV formatted chunk onto the CLI 30 | const OUTPUT_AS_CSV = false; 31 | // For debugging 32 | const DEBUG_ENABLED = false; 33 | // 1 (key type, account name, alias, label, comment, service, access control class, accessibility class, data) 34 | // 2 (key type, account name, alias, label, comment, service, access control class, accessibility class, access group, created on, modified on, data) 35 | // 3 (key type, UUID, account name, alias, label, comment, service, creator, access control class, accessibility class, access group, created on, modified on, encrytped data, data, sha1, associated with another key, hidden key, generic attribute, security domain, server, authentication type, path, port) 36 | // Affects both list and CSV formats 37 | const VERBOSITY_LEVEL = 2; 38 | 39 | //MARK: - Properties 40 | var SecItemCopyMatching = new NativeFunction(ptr(Module.findExportByName('Security', 'SecItemCopyMatching')), 'pointer', ['pointer', 'pointer']); 41 | var SecAccessControlGetConstraints = new NativeFunction(ptr(Module.findExportByName('Security', 'SecAccessControlGetConstraints')), 'pointer', ['pointer']); 42 | 43 | // This is the entry point for this script 44 | if(ObjC.available) { 45 | console.log(`${MAGENTA}[*] Enumerating Keychain keys for the application${RESET}\n`); 46 | try { 47 | main(); 48 | } catch(err) { 49 | console.log(`${RED}[!] An exception occured:${RESET} ${err.message}\n`); 50 | } 51 | } else { 52 | console.log(`${RED}[-] Objective-C runtime is not available${RESET}\n`); 53 | } 54 | 55 | // main is the starting function for the program 56 | function main() { 57 | var keychainKeys = []; 58 | 59 | if (DEBUG_ENABLED) { 60 | console.log(`${GREEN}Enumerating keys in Keychain${RESET}\n`); 61 | 62 | console.log(`${GREEN}${BOLD}DEBUG:${RESET}${GREEN}Raw Keychain contents${RESET}\n`); 63 | console.log(JSON.stringify(keychainKeys, null, 2)); 64 | } 65 | 66 | if (FILTER_ACCESSIBILITY_CLASS) { 67 | keychainKeys = filterSecureAccessibilityOnly(getKeychainContents()); 68 | } else { 69 | keychainKeys = getKeychainContents(); 70 | } 71 | 72 | if (OUTPUT_AS_LIST) { 73 | formatAsList(keychainKeys, VERBOSITY_LEVEL); 74 | } 75 | 76 | if (OUTPUT_AS_CSV) { 77 | formatAsCSV(keychainKeys, VERBOSITY_LEVEL); 78 | } 79 | } 80 | 81 | // Gets all the keys from the Keychain the application can access. Returns an array of key objects 82 | function getKeychainContents() { 83 | var keychainKeys = []; 84 | 85 | const keyTypes = [ 86 | 'keys', 87 | 'idnt', 88 | 'cert', 89 | 'genp', 90 | 'inet', 91 | ]; 92 | const kCFBooleanTrue = ObjC.classes.__NSCFBoolean.numberWithBool_(true); 93 | const queryDictionary = ObjC.classes.NSMutableDictionary.alloc().init(); 94 | queryDictionary.setObject_forKey_(kCFBooleanTrue, 'r_Attributes'); 95 | queryDictionary.setObject_forKey_(kCFBooleanTrue, 'r_Data'); 96 | queryDictionary.setObject_forKey_(kCFBooleanTrue, 'r_Ref'); 97 | queryDictionary.setObject_forKey_('m_LimitAll', 'm_Limit'); 98 | queryDictionary.setObject_forKey_('syna', 'sync'); 99 | 100 | keyTypes.forEach(function(keyType) { 101 | queryDictionary.setObject_forKey_(keyType, "class"); 102 | const keyPointer = Memory.alloc(Process.pointerSize); 103 | const copyResult = SecItemCopyMatching(queryDictionary, keyPointer); 104 | 105 | if (copyResult != 0x00) { 106 | return; 107 | } 108 | 109 | var searchResult = new ObjC.Object(Memory.readPointer(keyPointer)); 110 | 111 | /* 112 | { 113 | UUID = "5BF65C01-A7AD-440B-8CB2-F147241999BE"; 114 | accc = ""; 115 | acct = keychainValue; 116 | agrp = "UAVZNE8PJA.com.highaltitudehacks.DVIAswiftv2"; 117 | cdat = "2021-05-28 11:17:42 +0000"; 118 | class = genp; 119 | mdat = "2021-05-28 11:21:26 +0000"; 120 | musr = <>; 121 | pdmn = ak; 122 | persistref = <>; 123 | sha1 = <282a0d04 28a940b2 20f20019 0374a760 f51fbcd9>; 124 | svce = "com.highaltitudehacks.DVIAswiftv2"; 125 | sync = 0; 126 | tomb = 0; 127 | "v_Data" = <53656372 6574556e 69636f72 6e323432 34>; 128 | } 129 | */ 130 | 131 | if (searchResult.count() > 0) { 132 | // We have keys to extract 133 | for (var i = 0; i < searchResult.count(); i++) { 134 | // Now lets loop through each key 135 | var a = searchResult.objectAtIndex_(i); 136 | 137 | //console.log(a); 138 | 139 | // Okay, we need to extract the parts of the key and build a new object fo those extracts so we can use it later 140 | var key = { 141 | "UUID": getStringRep(a.objectForKey_("UUID")), 142 | "AccessControl": getAccessControlACLs(a), 143 | "Account": getStringRep(a.objectForKey_("acct")), 144 | "Alias": getStringRep(a.objectForKey_("alis")), 145 | "Label": getStringRep(a.objectForKey_("labl")), 146 | "Comment": getStringRep(a.objectForKey_("icmt")), 147 | "Creator": getStringRep(a.objectForKey_("crtr")), 148 | "AccessGroup": getStringRep(a.objectForKey_("agrp")), 149 | "CreatedOn": getStringRep(a.objectForKey_("cdat")), 150 | "Class": convertFourCharCode(keyType), 151 | "LastModified": getStringRep(a.objectForKey_("mdat")), 152 | "Accessibility": convertFourCharCode(getStringRep(a.objectForKey_("pdmn"))), 153 | "EncryptedData": getStringRep(a.objectForKey_("prot")), //https://opensource.apple.com/source/Security/Security-57740.1.18/OSX/libsecurity_keychain/lib/SecKeychainItemPriv.h 154 | "SHA1": formatToHexString(getStringRep(a.objectForKey_("sha1"))), 155 | "Service": getStringRep(a.objectForKey_("svce")), 156 | "Data": formatToHexString(getStringRep(a.objectForKey_("v_Data"))), 157 | "IsVisible": a.objectForKey_("invi"), 158 | "IsAssociatedToKey": a.objectForKey_('nega'), 159 | "GenericAttribute": getStringRep(a.objectForKey_("gena")), //genp only 160 | "SecurityDomain": getStringRep(a.objectForKey_('sdmn')), // inet only 161 | "Server": getStringRep(a.objectForKey_('srvr')), //inet only 162 | "AuthenticationType": getStringRep(a.objectForKey_("atyp")), // inet only 163 | "Port": getStringRep(a.objectForKey_("port")), // inet only 164 | "Path": getStringRep(a.objectForKey_("path")), //inet only 165 | }; 166 | 167 | keychainKeys.push(key); 168 | } 169 | } 170 | }); 171 | 172 | return keychainKeys; 173 | } 174 | 175 | // Gets the string representation of a property 176 | function getStringRep(obj) { 177 | try { 178 | var a = new ObjC.Object(obj); 179 | return Memory.readUtf8String(a.bytes(), a.length()); 180 | } catch (err) { 181 | try { 182 | return obj.toString(); 183 | } catch (err) { 184 | return 'null'; 185 | } 186 | } 187 | } 188 | 189 | // Takes a hex string and removes the spaces and special character, then returns the raw hex string 190 | function formatToHexString(str) { 191 | try { 192 | return str.replace(//g, "").replace(/\s/g, ""); 193 | } catch (_) { 194 | return "null"; 195 | } 196 | } 197 | 198 | // Takes a FourCharCode and returns it's string full name 199 | function convertFourCharCode(code) { 200 | switch(code) { 201 | case "genp": return "Generic Password"; 202 | case "inet": return "Internet Password"; 203 | case "cert": return "Certificate"; 204 | case "idnt": return "Identity"; 205 | case "keys": return "Cryptographic Key"; 206 | case "ak": return "kSecAttrAccessibleWhenUnlocked"; 207 | case "ck": return "kSecAttrAccessibleAfterFirstUnlock"; 208 | case "dk": return "kSecAttrAccessibleAlways"; 209 | case "aku": return "kSecAttrAccessibleWhenUnlockedThisDeviceOnly"; 210 | case "akpu": return "kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly"; 211 | case "cku": return "kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly"; 212 | case "dku": return "kSecAttrAccessibleAlwaysThisDeviceOnly"; 213 | } 214 | } 215 | 216 | function nullHandler(obj) { 217 | try { 218 | return obj.toString(); 219 | } catch (_) { 220 | return "null"; 221 | } 222 | } 223 | // Retreives the access control settings 224 | // https://opensource.apple.com/source/Security/Security-57336.10.29/OSX/sec/Security/SecAccessControlPriv.h.auto.html 225 | function getAccessControlACLs(accc) { 226 | if (!accc.containsKey_("accc")) { 227 | return "None"; 228 | } 229 | 230 | var acls = ObjC.Object(SecAccessControlGetConstraints(accc.objectForKey_("accc"))); 231 | 232 | if (acls.handle == 0x00) { 233 | return "None"; 234 | } 235 | 236 | //Use this for reverse engineering flags 237 | //console.log(acls); 238 | 239 | // At this point, I cannot find out what the individual keys in the ACL object mean, so need to do some reverse 240 | // engineering. But will use Senseposts (Objection) hook from 1.4.1 for now 241 | // https://github.com/sensepost/objection/blob/1.4.1/objection/hooks/ios/keychain/dump.js 242 | 243 | //TODO: Reverse engineer the ACL flags to determine what they all mean, then re-write this section. 244 | var flags = []; 245 | var acl_enumerator = acls.keyEnumerator(); 246 | var acl_item_key; 247 | 248 | while ((acl_item_key = acl_enumerator.nextObject()) !== null) { 249 | 250 | var acl_item = acls.objectForKey_(acl_item_key); 251 | 252 | switch (getStringRep(acl_item_key)) { 253 | 254 | case 'dacl': 255 | break; 256 | case 'osgn': 257 | flags.push('PrivateKeyUsage'); 258 | case 'od': 259 | var constraints = acl_item; 260 | var constraint_enumerator = constraints.keyEnumerator(); 261 | var constraint_item_key; 262 | 263 | while ((constraint_item_key = constraint_enumerator.nextObject()) !== null) { 264 | 265 | switch (getStringRep(constraint_item_key)) { 266 | case 'cpo': 267 | flags.push('kSecAccessControlUserPresence'); 268 | break; 269 | 270 | case 'cup': 271 | flags.push('kSecAccessControlDevicePasscode'); 272 | break; 273 | 274 | case 'pkofn': 275 | constraints.objectForKey_('pkofn') == 1 ? 276 | flags.push('Or') : 277 | flags.push('And'); 278 | break; 279 | 280 | case 'cbio': 281 | constraints.objectForKey_('cbio').count() == 1 ? 282 | flags.push('kSecAccessControlBiometryAny') : 283 | flags.push('kSecAccessControlBiometryCurrentSet'); 284 | break; 285 | 286 | default: 287 | break; 288 | } 289 | } 290 | break; 291 | case 'prp': 292 | flags.push('ApplicationPassword'); 293 | break; 294 | default: 295 | break; 296 | } 297 | } 298 | return flags.join(' '); 299 | } 300 | 301 | // Filters out all keys considered secure or exempt. Returns a filtered array of key objects 302 | function filterSecureClassesOnly(keysArray) { 303 | var originalKeys = keysArray; 304 | var filteredKeys = []; 305 | 306 | // Core goes here 307 | 308 | return filteredKeys; 309 | } 310 | 311 | // Turns an array of Keychain keys into a list displayed in the CLI. The verbosity level determines how much 312 | // data you see 313 | function formatAsList(keysArray, verbosityLevel = 2) { 314 | keysArray.forEach(function(object){ 315 | switch(verbosityLevel) { 316 | case 1: 317 | console.log(`Key Type: ${object.Class}`); 318 | console.log(`Account: ${object.Account}`); 319 | console.log(`Alias: ${object.Alias}`); 320 | console.log(`Label: ${object.Label}`); 321 | console.log(`Comment: ${object.Comment}`); 322 | console.log(`Service: ${object.Service}`); 323 | console.log(`Access Control: ${object.AccessControl}`); 324 | console.log(`Accessibility: ${object.Accessibility}`); 325 | console.log(`Data: ${object.Data}\n`); 326 | break; 327 | case 2: 328 | console.log(`Key Type: ${object.Class}`); 329 | console.log(`Account: ${object.Account}`); 330 | console.log(`Alias: ${object.Alias}`); 331 | console.log(`Label: ${object.Label}`); 332 | console.log(`Comment: ${object.Comment}`); 333 | console.log(`Service: ${object.Service}`); 334 | console.log(`Access Control: ${object.AccessControl}`); 335 | console.log(`Accessibility: ${object.Accessibility}`); 336 | console.log(`Access Group: ${object.AccessGroup}`); 337 | console.log(`Created on: ${object.CreatedOn}`); 338 | console.log(`Last modified on: ${object.LastModified}`); 339 | console.log(`Data: ${object.Data}\n`); 340 | break; 341 | case 3: 342 | console.log(`Key Type: ${object.Class}`); 343 | console.log(`UUID: ${object.UUID}`); 344 | console.log(`Account: ${object.Account}`); 345 | console.log(`Alias: ${object.Alias}`); 346 | console.log(`Label: ${object.Label}`); 347 | console.log(`Comment: ${object.Comment}`); 348 | console.log(`Service: ${object.Service}`); 349 | console.log(`Created by: ${object.Creator}`); 350 | console.log(`Access Control: ${object.AccessControl}`); 351 | console.log(`Accessibility: ${object.Accessibility}`); 352 | console.log(`Access Group: ${object.AccessGroup}`); 353 | console.log(`Created on: ${object.CreatedOn}`); 354 | console.log(`Last modified on: ${object.LastModified}`); 355 | console.log(`Encrypted data: ${object.EncryptedData}`); 356 | console.log(`Data: ${object.Data}`); 357 | console.log(`SHA1: ${object.SHA1}`); 358 | console.log(`Associated to another key: ${object.IsAssociatedToKey}`); 359 | console.log(`Hidden Key: ${object.IsVisible}`) 360 | 361 | if(object.Class == "Internet Password") { 362 | console.log(`Security Domain: ${object.SecurityDomain}`); 363 | console.log(`Server: ${object.Server}`); 364 | console.log(`Authentication Type: ${object.AuthenticationType}`); 365 | console.log(`Path: ${object.Path}`); 366 | console.log(`Port: ${object.Port}`); 367 | } 368 | 369 | if(object.Class == "Generic Password") { 370 | console.log(`Generic Attribute: ${object.GenericAttribute}`); 371 | } 372 | 373 | console.log("\n"); 374 | break; 375 | default: 376 | console.log(`Key Type: ${object.Class}`); 377 | console.log(`Account: ${object.Account}`); 378 | console.log(`Alias: ${object.Alias}`); 379 | console.log(`Label: ${object.Label}`); 380 | console.log(`Comment: ${object.Comment}`); 381 | console.log(`Service: ${object.Service}`); 382 | console.log(`Access Control: ${object.AccessControl}`); 383 | console.log(`Accessibility: ${object.Accessibility}`); 384 | console.log(`Data: ${object.Data}\n`); 385 | break; 386 | } 387 | }); 388 | } 389 | 390 | // formatAsCSV takes an array of Keychain objects and outputs a CSV formatted chunk of text to the CLI. verbosityLevel 391 | // determines how much information to include 392 | function formatAsCSV(keysArray, verbosityLevel = 1) { 393 | var content = ""; 394 | 395 | // CSV header row 396 | switch(verbosityLevel) { 397 | case 1: 398 | content += "keyType,account,alias,label,comment,service,accessControl,accessibility,data\n"; 399 | break; 400 | case 2: 401 | content += "keyType,account,alias,label,comment,service,accessControl,accessibility,accessGroup,createdOn,modifiedOn,data\n"; 402 | break; 403 | case 3: 404 | content += "keyType,uuid,account,alias,label,comment,service,createdBy,accessControl,accessibility,accessGroup,createdOn,modifiedOn,encrytpedData,data,sha1,associatedKey,hiddenKey,genericAttribute,securityDomain,server,authenticationType,path,port\n"; 405 | break; 406 | default: 407 | break; 408 | } 409 | 410 | keysArray.forEach(function(object) { 411 | switch(verbosityLevel) { 412 | case 1: 413 | content += `${object.Class},${object.Account},${object.Alias},${object.Label},${object.Comment},${object.Service},${object.AccessControl},${object.Accessibility},${object.Data}\n`; 414 | break; 415 | case 2: 416 | content += `${object.Class},${object.Account},${object.Alias},${object.Label},${object.Comment},${object.Service},${object.AccessControl},${object.Accessibility},${object.AccessGroup},${object.CreatedOn},${object.LastModified},${object.Data}\n`; 417 | break; 418 | case 3: 419 | content += `${object.Class},${object.UUID},${object.Account},${object.Alias},${object.Label},${object.Comment},${object.Service},${object.Creator},${object.AccessControl},${object.Accessibility},${object.AccessGroup},${object.CreatedOn},${object.LastModified},${object.EncryptedData},${object.Data},${object.SHA1},${object.IsAssociatedToKey},${object.IsVisible},${object.GenericAttribute},${object.SecurityDomain},${object.Server},${object.AuthenticationType},${object.Path},${object.Port}\n`; 420 | break; 421 | default: 422 | break; 423 | } 424 | }); 425 | 426 | console.log(`${GREEN}*** COPY EVERYTHING BETWEEN THESE GREEN LINES ***${RESET}`); 427 | console.log(content); 428 | console.log(`${GREEN}*** NOW SAVE AS A CSV FILE ***${RESET}`); 429 | } 430 | 431 | // This will filter out any keys with kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly is the accessibility class 432 | // Note that kSecAttrAccessibleAfterFirstUnlock can be used only when an application needs to access keys whilst it 433 | // is in the background. All others should be avoided 434 | function filterSecureAccessibilityOnly(keysArray) { 435 | const passcodeSet = "kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly"; 436 | var insecureObjects = []; 437 | 438 | keysArray.forEach(function(key) { 439 | if(key.Class !== passcodeSet) { insecureObjects.push(key)} 440 | }) 441 | 442 | return insecureObjects; 443 | } -------------------------------------------------------------------------------- /ios/EnumerateFileDataProtectionClasses.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Title: EnumerateFileDataProtectionClasses 3 | * 4 | * Description: This Frida script enumerates all the files and directories in an applications sandbox including their data 5 | * protection classes. Additionally, options can be enabled to filter out secure file system objects and create a CSV 6 | * output of the files found to be placed into evidence tables. 7 | * 8 | * Created by KittyNighthawk (2022) 9 | */ 10 | 11 | //MARK: - ANSII Colours 12 | const RESET = "\x1b[0m"; 13 | const BLACK = "\u001b[30m"; 14 | const RED = "\u001b[31m"; 15 | const GREEN = "\u001b[32m"; 16 | const YELLOW = "\u001b[33m"; 17 | const BLUE = "\u001b[34m"; 18 | const MAGENTA = "\u001b[35m"; 19 | const CYAN = "\u001b[36m"; 20 | const WHITE = "\u001b[37m"; 21 | const BOLD = "\u001b[1m"; 22 | 23 | //MARK: - Options 24 | // This will display the environment URLs for the application 25 | const SHOW_ENVIRONMENT_URLS = false; 26 | // This will output all environment URLs for the application (if SHOW_ENVIRONMENT_URLS is true) 27 | const SHOW_ALL_ENVIRONMENT_URLS = false; 28 | // This will filter out any file system objects that are considered secure or exempt 29 | const FILTER_DATA_PROTECTION_CLASSES = false; 30 | // This will output the results as a list on the CLI 31 | const OUTPUT_AS_LIST = true; 32 | // This will output a CSV formatted chunk onto the CLI 33 | const OUTPUT_AS_CSV = false; 34 | // For debugging 35 | const DEBUG_ENABLED = false; 36 | // 1 (name, type, URL, readable, writable, posixPermissions, data protection class) 37 | // 2 (name, type, URL, readable, writable, posixPermissions, owner, group, data protection class, created time, modified time) 38 | // Affects both list and CSV formats 39 | const VERBOSITY_LEVEL = 2; 40 | 41 | //MARK: - Properties 42 | const NSFileManager = ObjC.classes.NSFileManager; 43 | const NSString = ObjC.classes.NSString; 44 | const fileManager = NSFileManager.defaultManager(); 45 | const NSBundle = ObjC.classes.NSBundle; 46 | const bundleManager = NSBundle.mainBundle(); 47 | const regexMatchURLScheme = /(^\w+:|^)\/\//; 48 | 49 | //MARK: - NSSearchPathDirectory enums 50 | const NSSearchPathDirectory = { 51 | NSApplicationDirectory: 1, 52 | NSDemoApplicationDirectory: 2, 53 | NSDeveloperApplicationDirectory: 3, 54 | NSAdminApplicationDirectory: 4, 55 | NSLibraryDirectory: 5, 56 | NSDeveloperDirectory: 6, 57 | NSUserDirectory: 7, 58 | NSDocumentationDirectory: 8, 59 | NSDocumentDirectory: 9, 60 | NSCoreServiceDirectory: 10, 61 | NSAutosavedInformationDirectory: 11, 62 | NSDesktopDirectory: 12, 63 | NSCachesDirectory: 13, 64 | NSApplicationSupportDirectory: 14, 65 | NSDownloadsDirectory: 15, 66 | NSInputMethodsDirectory: 16, 67 | NSMoviesDirectory: 17, 68 | NSMusicDirectory: 18, 69 | NSPicturesDirectory: 19, 70 | NSPrinterDescriptionDirectory: 20, 71 | NSSharedPublicDirectory: 21, 72 | NSPreferencePanesDirectory: 22, 73 | NSApplicationScriptsDirectory: 23, 74 | NSItemReplacementDirectory: 99, 75 | NSAllApplicationsDirectory: 100, 76 | NSAllLibrariesDirectory: 101, 77 | NSTrashDirectory: 102, 78 | }; 79 | 80 | //MARK: - NSUSearchPathDomainMask enums 81 | const NSSearchPathDomainMask = { 82 | NSUserDomainMask: 1, 83 | NSLocalDomainMask: 2, 84 | NSNetworkDomainMask: 4, 85 | NSSystemDomainMask: 8, 86 | NSAllDomainsMask: 65535, 87 | }; 88 | 89 | //MARK: - NSFileAttributeType 90 | const NSFileAttributeType = { 91 | "NSFileTypeBlockSpecial": "Block special file", 92 | "NSFileTypeCharacterSpecial": "Character special file", 93 | "NSFileTypeDirectory": "Directory", 94 | "NSFileTypeRegular": "File", 95 | "NSFileTypeSocket": "Socket", 96 | "NSFileTypeSymbolicLink": "Symbolic link", 97 | "NSFileTypeUnknown": "Unknown", 98 | }; 99 | 100 | //MARK: - UID to Username 101 | const users = { 102 | "-2": "nobody", 103 | "0": "root", 104 | "501": "mobile", 105 | "1": "daemon", 106 | "98": "_ftp", 107 | "24": "_networkd", 108 | "25": "_wireless", 109 | "33": "_installd", 110 | "34": "_neagent", 111 | "35": "_ifccd", 112 | "64": "_securityd", 113 | "65": "_mdnsresponder", 114 | "75": "_sshd", 115 | "99": "_unknown", 116 | "241": "_distnote", 117 | "245": "_astris", 118 | "249": "_ondemand", 119 | "254": "_findmydevice", 120 | "257": "_datadetectors", 121 | "258": "_captiveagent", 122 | "263": "_analyticsd", 123 | "266": "_timed", 124 | "267": "_gpsd", 125 | "268": "_nearbyd", 126 | "269": "_reportmemoryexception", 127 | }; 128 | 129 | // fsObjects holds enumerated fsObjects 130 | var fsObjects = []; 131 | 132 | console.log(`${MAGENTA}[*] Enumerating filesystem objects for the application${RESET}\n`); 133 | 134 | // This is the entry point for this script 135 | if(ObjC.available) { 136 | try { 137 | main(); 138 | } catch(err) { 139 | console.log(`${RED}[!] An exception occured: ${RESET}${err.message}\n`); 140 | } 141 | } else { 142 | console.log(`${RED}[-] Objective-C runtime is not available${RESET}`); 143 | } 144 | 145 | // getURLForLocation takes in a NSSearchPathDirectory (int) and NSSearchPathDomainMask (int) and returns the URL for that location 146 | function getURLForLocation(searchPathDirectory, searchPathDomainMask) { 147 | const url = fileManager.URLsForDirectory_inDomains_(searchPathDirectory, searchPathDomainMask).lastObject(); 148 | 149 | if(url) { 150 | // Remove protocol handler (file://) if it is present 151 | return cleanURL(url.toString()); 152 | } else { 153 | return "-"; 154 | } 155 | } 156 | 157 | // cleanURL takes in a URL string and removes any URL schemes from the beginning of a URL 158 | function cleanURL(url) { 159 | // First, check that there is a URL scheme in the url 160 | if (regexMatchURLScheme.exec(url)) { 161 | // If there is, replace the matched group with nothing 162 | return url.replace(regexMatchURLScheme, ''); 163 | } else{ 164 | // Otherwise, there is no URL scheme so just return the url 165 | return url; 166 | } 167 | } 168 | 169 | // printCorePaths will print out the environment URLs for the app. allPaths determines whether to display the basic paths 170 | // or all possible paths 171 | function printCorePaths(allPaths = false) { 172 | if (!allPaths) { 173 | console.log(`${GREEN}[*] Application environment URLs:${RESET}`); 174 | console.log(`${GREEN}[+] Bundle: ${bundleManager.bundlePath()}${RESET}`); 175 | console.log(`${GREEN}[+] Documents: ${getURLForLocation(NSSearchPathDirectory.NSDocumentDirectory, NSSearchPathDomainMask.NSUserDomainMask)}${RESET}`); 176 | console.log(`${GREEN}[+] Library: ${getURLForLocation(NSSearchPathDirectory.NSLibraryDirectory, NSSearchPathDomainMask.NSUserDomainMask)}${RESET}`); 177 | console.log(""); 178 | } else { 179 | console.log(`${GREEN}[*] Application environment URLs:${RESET}`) 180 | for (var key in NSSearchPathDirectory) { 181 | const path = getURLForLocation(NSSearchPathDirectory[key], NSSearchPathDomainMask.NSUserDomainMask); 182 | if(path) { 183 | console.log(`${GREEN}[+] ${key}: ${path}${RESET}`); 184 | } else { 185 | console.log(`${GREEN}[+] ${key}: -${RESET}`); 186 | } 187 | } 188 | console.log(""); 189 | } 190 | } 191 | 192 | // getDirectoryContents will enumerate all the file system objects from the specified URL and output their attributes 193 | function getDirectoryContents(url) { 194 | var contents = fileManager.contentsOfDirectoryAtPath_error_(url, NULL); 195 | var recurse = true; 196 | 197 | // Create an array 198 | var fsObjects = []; 199 | 200 | // For each item in the directory, create a new object 201 | var numberOfObjects; 202 | 203 | if (contents !== null) { 204 | numberOfObjects = contents.count(); 205 | } else { 206 | numberOfObjects = 0; 207 | } 208 | 209 | for(var i = 0; i < numberOfObjects; i++) { 210 | var file = contents.objectAtIndex_(i); 211 | 212 | var fsObject = { 213 | name: file.toString(), 214 | type: 'Unknown', 215 | isDirectory: null, 216 | url: url + "/" + file.toString(), 217 | owner: 'Unknown', 218 | group: 'Unknown', 219 | readable: false, 220 | writable: false, 221 | posixPermissions: 'Unknown', 222 | dataProtectionClass: '', 223 | createdTime: 'Unknown', 224 | modifiedTime: 'Unknown', 225 | allAttributes: null, 226 | }; 227 | 228 | var read = fileManager.isReadableFileAtPath_(fsObject.url); 229 | var write = fileManager.isWritableFileAtPath_(fsObject.url); 230 | 231 | fsObject.readable = read; 232 | fsObject.writable = write; 233 | 234 | var attributes = fileManager.attributesOfItemAtPath_error_(fsObject.url, NULL); 235 | if (attributes) { 236 | fsObject.allAttributes = attributes; 237 | var enumerator = attributes.keyEnumerator(); 238 | 239 | var key; 240 | while ((key = enumerator.nextObject()) !== null) { 241 | if (key == "NSFileProtectionKey") { 242 | var value = attributes.objectForKey_(key); 243 | if (value) { 244 | fsObject.dataProtectionClass = value; 245 | } else { 246 | fsObject.dataProtectionClass = "NSFileProtectionNone"; 247 | } 248 | } else if (key == "NSFileType") { 249 | var value = attributes.objectForKey_(key); 250 | if (value) { 251 | fsObject.type = NSFileAttributeType[value]; 252 | if (value == NSFileAttributeType["NSFileTypeDirectory"]) { 253 | fsObject.isDirectory = true; 254 | } else { 255 | fsObject.isDirectory = false; 256 | } 257 | } 258 | } else if (key == "NSFileOwnerAccountName") { 259 | var value = attributes.objectForKey_(key); 260 | if (value) { 261 | fsObject.owner = value; 262 | } 263 | } else if (key == "NSFileGroupOwnerAccountName") { 264 | var value = attributes.objectForKey_(key); 265 | if (value) { 266 | fsObject.group = value; 267 | } 268 | } else if (key == "NSFilePosixPermissions") { 269 | var value = attributes.objectForKey_(key); 270 | if (value) { 271 | fsObject.posixPermissions = parseInt(value.toString()).toString(8); 272 | } 273 | } else if (key == "NSFileCreationDate") { 274 | var value = attributes.objectForKey_(key); 275 | if (value) { 276 | fsObject.createdTime = value; 277 | } 278 | } else if (key == "NSFileModificationDate") { 279 | var value = attributes.objectForKey_(key); 280 | if (value) { 281 | fsObject.modifiedTime = value; 282 | } 283 | } else { continue; } 284 | } 285 | } 286 | 287 | /* Example attributes 288 | NSFileOwnerAccountID 501 289 | NSFileSystemFileNumber 12885684640 290 | NSFileExtensionHidden 0 291 | NSFileSystemNumber 16777221 292 | NSFileSize 96 293 | NSFileGroupOwnerAccountID 501 294 | NSFileOwnerAccountName mobile 295 | NSFileCreationDate 2020-08-04 16:04:25 +0000 296 | NSFilePosixPermissions 493 297 | NSFileProtectionKey NSFileProtectionComplete 298 | NSFileType NSFileTypeDirectory 299 | NSFileGroupOwnerAccountName mobile 300 | NSFileReferenceCount 3 301 | NSFileModificationDate 2020-08-04 16:04:40 +0000 302 | */ 303 | 304 | // Clean the URL by removing any double slashes 305 | fsObject.url = fsObject.url.replace(/\/\/+/g, '/'); 306 | fsObjects.push(fsObject); 307 | 308 | if (DEBUG_ENABLED) { 309 | console.log(`${GREEN}[[DBG]] Name: ${fsObject.name}${RESET}`); 310 | console.log(`${GREEN}[[DBG]] Type: ${fsObject.type}${RESET}`); 311 | console.log(`${GREEN}[[DBG]] URL: ${fsObject.url}${RESET}`); 312 | console.log(`${GREEN}[[DBG]] Owner: ${fsObject.owner}${RESET}`); 313 | console.log(`${GREEN}[[DBG]] Group: ${fsObject.group}${RESET}`); 314 | console.log(`${GREEN}[[DBG]] Readable: ${fsObject.readable}${RESET}`); 315 | console.log(`${GREEN}[[DBG]] Writable: ${fsObject.writable}${RESET}`); 316 | console.log(`${GREEN}[[DBG]] Permissions: ${fsObject.posixPermissions}${RESET}`); 317 | console.log(`${GREEN}[[DBG]] Created: ${fsObject.createdTime}${RESET}`); 318 | console.log(`${GREEN}[[DBG]] Modified: ${fsObject.modifiedTime}${RESET}`); 319 | if (fsObject.dataProtectionClass != "") { 320 | console.log(`${GREEN}[[DBG]] Data protection class: ${fsObject.dataProtectionClass}${RESET}`); 321 | } else { 322 | console.log(`${GREEN}[[DBG]] Data protection class: NSFileProtectionNone${RESET}`); 323 | } 324 | console.log(); 325 | } 326 | 327 | if (fsObject.dataProtectionClass == "" || fsObject.dataProtectionClass == "None") { 328 | fsObject.dataProtectionClass = "NSFileProtectionNone"; 329 | } 330 | 331 | if (recurse) { 332 | if (fsObject.type == "Directory") { 333 | fsObjects.push(getDirectoryContents(fsObject.url)); 334 | } 335 | } 336 | } 337 | return fsObjects; 338 | } 339 | 340 | // formatAsCSV takes an array of file system objects and outputs a CSV formatted chunk of text to the CLI. verbosityLevel 341 | // determines how much information to include 342 | function formatAsCSV(fsObjects, verbosityLevel = 1) { 343 | var content = ""; 344 | 345 | if (verbosityLevel == 1) { 346 | content += "name,type,url,readable,writable,posixPermissions,data-protection-class"; 347 | } else if (verbosityLevel == 2) { 348 | content += "name,type,url,owner,group,readable,writable,posixPermissions,data-protection-class,createOn,lastModifiedOn"; 349 | } 350 | 351 | console.log(content); 352 | 353 | var recurseFsObject = function (arr) { 354 | arr.forEach(function (fsObject) { 355 | if (typeof fsObject == 'object' && fsObject.constructor === Array) { 356 | recurseFsObject(fsObject); 357 | } 358 | if (typeof fsObject.type !== 'undefined') { 359 | switch (verbosityLevel) { 360 | case 1: 361 | content = fsObject.name + ","; 362 | content += fsObject.type + ","; 363 | content += fsObject.url + ","; 364 | content += fsObject.readable + ","; 365 | content += fsObject.writable + ","; 366 | content += fsObject.posixPermissions + ","; 367 | content += fsObject.dataProtectionClass; 368 | break; 369 | case 2: 370 | content = fsObject.name + ","; 371 | content += fsObject.type + ","; 372 | content += fsObject.url + ","; 373 | content += fsObject.owner + ","; 374 | content += fsObject.group + ","; 375 | content += fsObject.readable + ","; 376 | content += fsObject.writable + ","; 377 | content += fsObject.posixPermissions + ","; 378 | content += fsObject.dataProtectionClass + ","; 379 | content += fsObject.createdTime + ","; 380 | content += fsObject.modifiedTime; 381 | break; 382 | default: 383 | console.log(`${RED}[ERROR] There was an error formatting the CSV content${RESET}`); 384 | break; 385 | } 386 | } 387 | console.log(content); 388 | }); 389 | } 390 | console.log(`${GREEN}*** COPY EVERYTHING BETWEEN THESE GREEN LINES ***${RESET}`); 391 | recurseFsObject(fsObjects); 392 | console.log(`${GREEN}*** NOW SAVE AS A CSV FILE ***${RESET}`); 393 | } 394 | 395 | // Turns an array of file system objects into a list displayed in the CLI. The verbosity level determines how much 396 | // data you see 397 | function formatAsList(fsObjects, verbosityLevel = 1) { 398 | var recurseFsObject = function (arr, verbosityLevel) { 399 | arr.forEach(function (fsObject) { 400 | if (typeof fsObject == 'object' && fsObject.constructor === Array) { 401 | recurseFsObject(fsObject, verbosityLevel); 402 | } else if (typeof fsObject.type !== 'undefined') { 403 | switch (verbosityLevel) { 404 | case 1: 405 | console.log(`Name: ${fsObject.name}`); 406 | console.log(`Type: ${fsObject.type}`); 407 | console.log(`URL: ${fsObject.url}`); 408 | console.log(`Readable: ${fsObject.readable}`); 409 | console.log(`Writable: ${fsObject.writable}`); 410 | console.log(`Permissions: ${fsObject.posixPermissions}`); 411 | if (fsObject.dataProtectionClass != "") { 412 | console.log(`Data protection class: ${fsObject.dataProtectionClass}`); 413 | } else { 414 | console.log("Data protection class: None"); 415 | } 416 | console.log(); 417 | break; 418 | case 2: 419 | console.log(`Name: ${fsObject.name}`); 420 | console.log(`Type: ${fsObject.type}`); 421 | console.log(`URL: ${fsObject.url}`); 422 | console.log(`Owner: ${fsObject.owner}`); 423 | console.log(`Group: ${fsObject.group}`); 424 | console.log(`Readable: ${fsObject.readable}`); 425 | console.log(`Writable: ${fsObject.writable}`); 426 | console.log(`Permissions: ${fsObject.posixPermissions}`); 427 | console.log(`Created: ${fsObject.createdTime}`); 428 | console.log(`Modified: ${fsObject.modifiedTime}`); 429 | if (fsObject.dataProtectionClass != "") { 430 | console.log(`Data protection class: ${fsObject.dataProtectionClass}`); 431 | } else { 432 | console.log("Data protection class: None"); 433 | } 434 | console.log(); 435 | break; 436 | default: 437 | console.log("No data to display"); 438 | } 439 | } else { 440 | // 441 | } 442 | }); 443 | } 444 | 445 | recurseFsObject(fsObjects, verbosityLevel); 446 | } 447 | 448 | // Filters an fsObject array and returns a new array containing only those objects considered insecure 449 | function filterSecureClassesOnly(fsObjects) { 450 | // These are the Data Protection Classes deemed insecure that should not be used 451 | const insecureDataProtectionClasses = [ 452 | "NSFileProtectionNone", 453 | "NSFileProtectionCompleteUntilFirstUserAuthentication" 454 | ]; 455 | 456 | // These are paths that need to be "insecure" so that iOS/iPadOS can access them for normal functionality 457 | const excludedPaths = [ 458 | "/Cache.db", 459 | "/Cache.db-shm", 460 | "/Cache.db-wal", 461 | "/KnownSceneSessions/data.data", 462 | "/Library/SplashBoard/Snapshots/", 463 | "/SystemData/", 464 | ".com.apple.mobile_container_manager.metadata.plist" 465 | ]; 466 | 467 | var insecureFsObjects = []; 468 | 469 | var recurseFsObject = function (arr) { 470 | arr.forEach(function (fsObject) { 471 | if (typeof fsObject == 'object' && fsObject.constructor === Array) { 472 | recurseFsObject(fsObject); 473 | } 474 | 475 | if (typeof fsObject.type !== 'undefined') { 476 | insecureDataProtectionClasses.forEach(function (protectionClass) { 477 | if (fsObject.dataProtectionClass == protectionClass) { 478 | insecureFsObjects.push(fsObject); 479 | } 480 | }); 481 | } 482 | }); 483 | } 484 | recurseFsObject(fsObjects); 485 | return insecureFsObjects; 486 | } 487 | 488 | // Helper function to get the root URL of the application 489 | function getAppRootPath(url) { 490 | var urlParts = url.split("/"); 491 | urlParts.pop(); 492 | urlParts.pop(); 493 | return urlParts.join("/"); 494 | } 495 | 496 | // main is the starting function for the program 497 | function main() { 498 | var applicationRootPath = getAppRootPath(getURLForLocation(NSSearchPathDirectory.NSDocumentDirectory, NSSearchPathDomainMask.NSUserDomainMask)); 499 | var fsItems = []; 500 | 501 | if (DEBUG_ENABLED) { 502 | console.log(`${GREEN}Enumerating objects from: ${applicationRootPath}${RESET}`); 503 | } 504 | 505 | if (SHOW_ENVIRONMENT_URLS) { 506 | printCorePaths(SHOW_ALL_ENVIRONMENT_URLS); 507 | } 508 | 509 | if (FILTER_DATA_PROTECTION_CLASSES) { 510 | fsItems = filterSecureClassesOnly(getDirectoryContents(getURLForLocation(applicationRootPath))); 511 | } else { 512 | fsItems = getDirectoryContents(applicationRootPath); 513 | } 514 | 515 | if (OUTPUT_AS_LIST) { 516 | formatAsList(fsItems, VERBOSITY_LEVEL); 517 | } 518 | 519 | if (OUTPUT_AS_CSV) { 520 | formatAsCSV(fsItems, VERBOSITY_LEVEL); 521 | } 522 | } 523 | --------------------------------------------------------------------------------