Created
April 3, 2020 13:14
-
-
Save ztgrace/bea1fedfef735988ca0b375ed2e9a930 to your computer and use it in GitHub Desktop.
Fuzz Universal Links on iOS 12
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* iOS URI Scheme Fuzzing | |
* forked from: https://codeshare.frida.re/@dki/ios-url-scheme-fuzzing/ | |
* Usage: frida -U -l ios-uri-scheme-fuzzing.js -n SpringBoard | |
* | |
* Open the specified URL | |
* openURL("somescheme://test"); | |
* | |
* Find the executable name for a particular scheme | |
* bundleExecutableForScheme("somescheme"); | |
* | |
* Emulate single home button click (for app backgrounding) | |
* homeSinglePress(); | |
* | |
* Fuzz a particular URL - use {0} as placeholder for insertion points | |
* fuzz("somescheme://somepath?param={0}"); | |
* | |
* Move all crash logs matching a particular string to /tmp | |
* moveCrashLogs("MyApp"); | |
* | |
* | |
* You'll typically want to call openURL for the target scheme once before | |
* fuzzing to dismiss the prompt that appears the first time | |
* | |
* openURL("somescheme://test"); | |
* fuzzStrings.push("somefancyfuzzstring"); | |
* fuzz("somescheme://test/{0}"); | |
* | |
* Modifications: | |
* - changed logic in homeSinglePress to work with iOS 12 | |
* - added additional logging to better associate crashes to payloads | |
* | |
*/ | |
// have Springboard open a URL with whatever handler has claimed it | |
function openURL(url) { | |
var w = ObjC.classes.LSApplicationWorkspace.defaultWorkspace(); | |
var toOpen = ObjC.classes.NSURL.URLWithString_(url); | |
return w.openSensitiveURL_withOptions_(toOpen, null); | |
} | |
// emulate single home button press | |
function homeSinglePress() { | |
var version = ObjC.classes.UIDevice.currentDevice().systemVersion().toString(); | |
ObjC.schedule(ObjC.mainQueue, function() { | |
ObjC.classes.SBUIController.sharedInstance().handleHomeButtonSinglePressUp(); | |
}); | |
} | |
// check for crash logs and move them to /tmp/ if they exist | |
// can result in false positives if the appname is a substring of another app | |
function moveCrashLogs(appname) { | |
var match = appname + "*.ips"; | |
var pred = ObjC.classes.NSPredicate.predicateWithFormat_('SELF like "' + match + '"'); | |
var fm = ObjC.classes.NSFileManager.defaultManager(); | |
var dirlist = fm.contentsOfDirectoryAtPath_error_("/private/var/mobile/Library/Logs/CrashReporter", NULL); | |
var results = dirlist.filteredArrayUsingPredicate_(pred); | |
if (results.count() > 0) { | |
for (var i = 0; i < results.count(); i++) { | |
var src = results.objectAtIndex_(i).toString(); | |
fm.moveItemAtPath_toPath_error_("/private/var/mobile/Library/Logs/CrashReporter/" + src, "/tmp/" + src, NULL); | |
console.log('Crash detected - ' + src); | |
} | |
return true; | |
} | |
return false; | |
} | |
// https://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format | |
// this is what happens when i port things from python D: | |
if (!String.format) { | |
String.format = function(format) { | |
var args = Array.prototype.slice.call(arguments, 1); | |
return format.replace(/{(\d+)}/g, function(match, number) { | |
return typeof args[number] != 'undefined' ? | |
args[number] : | |
match; | |
}); | |
}; | |
} | |
// add/remove default fuzz strings here, or at the command line | |
var fuzzStrings = ["0", | |
"1", | |
"-1", | |
"null", | |
"nil", | |
"99999999999999999999999999999999999", | |
Array(257).join("A"), | |
Array(1026).join("A"), | |
"'", | |
"%20d", | |
"%20n", | |
"%20x", | |
"%20s", | |
]; | |
fuzzStrings.iter = function() { | |
var index = 0; | |
var data = this; | |
return { | |
next: function() { | |
return { | |
value: data[index], | |
done: index++ == (data.length - 1) | |
}; | |
}, | |
hasNext: function() { | |
return index < data.length; | |
} | |
} | |
}; | |
// query the name of the executable (process name) for a URL scheme | |
function bundleExecutableForScheme(scheme) { | |
var apps = ObjC.classes.LSApplicationWorkspace.defaultWorkspace().applicationsAvailableForHandlingURLScheme_(scheme); | |
// if there are multiple apps, punt | |
if (apps.count() != 1) { | |
return null; | |
} | |
var appProxy = apps.objectAtIndex_(0); // LSApplicationProxy | |
var bundleExecutable = appProxy.bundleExecutable(); | |
if (bundleExecutable !== null) { | |
return bundleExecutable.toString(); | |
} | |
return null; | |
} | |
// dump all registered URL schemes, organized by process name | |
// this no longer works on iOS 11 :( | |
function dumpSchemes() { | |
var map = {}; | |
var schemes = ObjC.classes.LSApplicationWorkspace.defaultWorkspace().publicURLSchemes(); | |
for (var i = 0; i < schemes.count(); i++) { | |
var name = bundleExecutableForScheme(schemes.objectAtIndex_(i)); | |
if (!(name in map)) { | |
map[name] = []; | |
} | |
map[name].push(schemes.objectAtIndex_(i).toString()); | |
} | |
return map; | |
} | |
// fuzz a url for a registered scheme | |
// use {0} for placeholders: blah://test/{0} | |
function fuzz(url) { | |
// find the process name for this url scheme | |
var appname = bundleExecutableForScheme(url.split(':')[0]); | |
if (appname === null) { | |
console.log("Could not determine which app handles this URL!"); | |
return; | |
} | |
function Fuzzer(url, appname, iter) { | |
this.url = url; | |
this.appname = appname; | |
this.iter = iter; | |
} | |
Fuzzer.prototype.checkForCrash = function(done) { | |
// background in case it is still running | |
homeSinglePress(); | |
// check for a crash | |
moveCrashLogs(this.appname) | |
// fuzz next url | |
if (!done) { | |
this.fuzz(); | |
} | |
}; | |
Fuzzer.prototype.fuzz = function() { | |
var term = this.iter.next(); | |
// create the url | |
var fuzzedURL = String.format(this.url, term.value); | |
// this should launch the app | |
if (openURL(fuzzedURL)) { | |
console.log("Opened URL: " + fuzzedURL); | |
} else { | |
console.log("URL refused by SpringBoard: " + fuzzedURL); | |
} | |
// wait a few seconds before backgrounding | |
ObjC.classes.NSThread.sleepForTimeInterval_(3); | |
this.checkForCrash(term.done); | |
}; | |
console.log("Watching for crashes from " + appname + "..."); | |
// start by clearing any current logs | |
if (moveCrashLogs(appname)) { | |
console.log("Moved one or more logs to /tmp/ before fuzzing!"); | |
} | |
// get iterator for fuzz strings | |
var iter = fuzzStrings.iter(); | |
var fuzzer = new Fuzzer(url, appname, iter); | |
fuzzer.fuzz(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment