Skip to content

Commit

Permalink
Merge pull request #186 from mwrlabs/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
marco-lancini authored Jul 13, 2017
2 parents 90c3d54 + cb839bc commit ea5a305
Show file tree
Hide file tree
Showing 12 changed files with 151 additions and 60 deletions.
17 changes: 13 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
*.pyc
.idea
# Supporting files
.DS_Store
*.localconf
_private/
.idea/

# Compiled python modules
*.pyc

# Setuptools distribution folder
/dist/
/build/

# Python egg metadata, regenerated from source files by setuptools.
/*.egg-info
.eggs/
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).



## Unreleased
#### Fixed
- **[CORE]** Search PID for System Apps
- **[MODULE]** Keychain extraction of data not encodable in UTF8 _[from @federicodotta]_
- **[MODULE]** Improved jailbreak detection bypass (`dynamic/detection/script_jailbreak-detection-bypass.py`)
- **[MODULE]** Improved certificate pinning bypass (`comms/proxy/pinning_bypass_frida`)



## [1.3.1] - 2017-06-22
#### Fixed
- **[CORE]** Asyncore problems _[from @floyd-fuh]_
Expand Down Expand Up @@ -46,6 +55,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- **[CORE]** Remove infinite loop from `Retry` decorator, which attempts to restore a connection with the device if it fails
- **[CORE]** Metadata parsing for app extensions
- **[CORE]** Re-added support on iOS for: `storage/data/keychain_dump`, `binary/reversing/strings`, `binary/reversing/class_dump`
- **[CORE]** Use unquote to convert spaces. Fixes Issue #15 _[from @ccsplit]_



Expand Down
8 changes: 5 additions & 3 deletions needle/core/device/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ def __init__(self, host, port):
raise se

def close(self):
self.socket.close()
if self.socket:
self.socket.close()

def send_to_device(self, cmd, marker=Constants.AGENT_OUTPUT_END):
self.socket.send(cmd + '\r\n')
Expand Down Expand Up @@ -53,8 +54,9 @@ def connect(self):
self._device.printer.notify("{} Successfully connected to agent ({}:{})...".format(Constants.AGENT_TAG, self._ip, self._port))

def disconnect(self):
self._device.printer.verbose("{} Disconnecting from agent...".format(Constants.AGENT_TAG))
self.client.close()
if self.client:
self._device.printer.verbose("{} Disconnecting from agent...".format(Constants.AGENT_TAG))
self.client.close()

@Retry()
def exec_command_agent(self, cmd):
Expand Down
21 changes: 14 additions & 7 deletions needle/core/device/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def _retrieve_metadata(self):
metadata_agent = self.__parse_from_agent()

# Content of the app's local Info.plist
plist_info_path = Utils.escape_path('%s/Info.plist' % metadata_agent['binary_directory'])
plist_info_path = Utils.escape_path('%s/Info.plist' % metadata_agent['binary_directory'], escape_accent=True)
plist_info = self._device.remote_op.parse_plist(plist_info_path)
metadata_info = self.__parse_plist_info(plist_info)

Expand Down Expand Up @@ -54,9 +54,9 @@ def __parse_from_agent(self):
name = self.__extract_field(agent_info, 'DisplayName').encode('ascii','replace')
bundle_type = self.__extract_field(agent_info, 'BundleType')
bundle_id = self.__extract_field(agent_info, 'BundleIdentifier')
data_directory = self.__extract_field(agent_info, 'DataContainer', path=True)
bundle_directory = self.__extract_field(agent_info, 'BundleContainer', path=True)
binary_directory = self.__extract_field(agent_info, 'BundleURL', path=True)
data_directory = self.__extract_field(agent_info, 'DataContainer', path=True, urldecode=True)
bundle_directory = self.__extract_field(agent_info, 'BundleContainer', path=True, urldecode=True)
binary_directory = self.__extract_field(agent_info, 'BundleURL', path=True, urldecode=True)
app_version = self.__extract_field(agent_info, 'BundleVersion')
sdk_version = self.__extract_field(agent_info, 'SDKVersion')
entitlements = self.__extract_field(agent_info, 'Entitlements')
Expand Down Expand Up @@ -120,14 +120,21 @@ def __detect_architectures(self, binary):
res = msg.rsplit(': ')[-1].split(' ')
return res

def __extract_field(self, data, field, path=False):
def __extract_field(self, data, field, path=False, urldecode=False):
"""Extract the specified entry from the plist file. Returns empty string if not present."""
try:
temp = data[field]
if urldecode:
try:
from urllib.parse import unquote
except ImportError:
# Python 2.x
from urllib import unquote
temp = unquote(temp)
if path:
prefix = 'file://'
if temp.startswith(prefix):
temp = temp[len(prefix):]
temp = Utils.escape_path(temp[len(prefix):])
return temp
except:
return ""
Expand Down Expand Up @@ -179,7 +186,7 @@ def search_pid(self, binary_name):
cmd = "ps ax | grep -i '{binary_name}'".format(binary_name=binary_name)
out = self._device.remote_op.command_blocking(cmd)
try:
process_list = filter(lambda x: '/var/mobile' in x, out)
process_list = filter(lambda x: 'grep' not in x, out)
if not process_list:
process_list = filter(lambda x: '/var/containers' in x, out)
process = process_list[0].strip()
Expand Down
2 changes: 1 addition & 1 deletion needle/core/framework/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ class FridaScript(FridaModule):
def __init__(self, params):
FridaModule.__init__(self, params)
# Add option for launch mode
opt = ('spawn', True, True, 'If set to True, Frida will be used to spawn the app. '
opt = ('spawn', False, True, 'If set to True, Frida will be used to spawn the app. '
'If set to False, the app will be launched and Frida will be attached to the running instance')
self.register_option(*opt)
opt = ('resume', True, True, 'If set to True, Frida will resume the application process after spawning it (recommended)')
Expand Down
6 changes: 5 additions & 1 deletion needle/core/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ class Constants(object):
# ==================================================================================================================
# AUTHOR
AUTHOR = 'MWR InfoSecurity (@MWRLabs) - Marco Lancini (@LanciniMarco)'
EMAIL = 'marco.lancini[at]mwrinfosecurity.com'
EMAIL = 'marco.lancini@mwrinfosecurity.com'
WEBSITE = 'mwr.to/needle'
VERSION = '1.3.1'
VERSION_CHECK = 'https://raw.githubusercontent.com/mwrlabs/needle/master/needle/core/utils/constants.py'

# Name variables
NAME = 'Needle'
DESCRIPTION = 'The iOS Security Testing Framework'
NAME_FOLDER = '.needle'
NAME_CLI = '%s[needle]%s > ' % (Colors.C, Colors.N)

Expand Down Expand Up @@ -49,6 +50,7 @@ class Constants(object):
# AGENT CONSTANTS
AGENT_TAG = "[AGENT]"
AGENT_WELCOME = "Welcome to Needle Agent"
AGENT_BUNDLE_ID = "mwr.needle.agent"
AGENT_VERSION_MARK = "VERSION: "
AGENT_OUTPUT_END = " :OUTPUT_END:"
AGENT_TIMEOUT_READ = 5
Expand All @@ -61,6 +63,8 @@ class Constants(object):
'10': [
'binary/installation/install',
'binary/installation/pull_ipa',
'binary/reversing/class_dump',
'binary/reversing/strings'
]
}

Expand Down
6 changes: 5 additions & 1 deletion needle/core/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ class Utils(object):
# PATH UTILS
# ==================================================================================================================
@staticmethod
def escape_path(path):
def escape_path(path, escape_accent=False):
"""Escape the given path."""
import pipes
path = path.strip() # strip
path = path.strip(''''"''') # strip occasional single/double quotes from both sides
if escape_accent:
# Find the accents/backquotes that do not have a backslash
# in front of them and escape them.
path = re.sub('(?<!\\\\)`', '\`', path)
return pipes.quote(path)

@staticmethod
Expand Down
1 change: 1 addition & 0 deletions needle/modules/binary/info/provisioning_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(self, params):
self.options['output'] = self.local_op.build_output_path_for_file("provisioning_profile", self)

def _parse_certificate(self, data):
self.printer.verbose("Parsing the certificate...")
# Read the plist file
pl = Utils.plist_read_from_file(data, use_plistlib=True)
# Extract the Data field of the certificate and store it locally
Expand Down
12 changes: 12 additions & 0 deletions needle/modules/comms/proxy/pinning_bypass_frida.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ class Module(FridaScript):
return SSLSetSessionOption(context, option, value);
}, 'int', ['pointer', 'int', 'bool']));
// iOS 10
var tls_helper_create_peer_trust = new NativeFunction(
Module.findExportByName(null, "tls_helper_create_peer_trust"),
'int',
['pointer', 'bool', 'pointer']
);
Interceptor.replace(tls_helper_create_peer_trust, new NativeCallback(function(hdsk, server, trustRef) {
return noErr;
}, 'int', ['pointer', 'bool', 'pointer']));
//
// OLD WAY
//
Expand Down
2 changes: 1 addition & 1 deletion needle/modules/device/dependency_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def _check_prerequisites(self):
for tool in Constants.DEVICE_SETUP['PREREQUISITES']:
if not self.__is_tool_available(tool):
self.device.printer.error('Prerequisite Not Found: %s ' % tool)
raise Exception('Please install the requirements listed in the README file')
raise Exception('Please install the requirements listed in the project WIKI')

def _refresh_package_list(self):
"""Refresh the list of installed packages."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,44 @@ class Module(FridaScript):
}

JS = '''\
var funcs = [];
var paths= [
"/pguntether",
"/usr/sbin/frida-server",
"/usr/bin/cycript",
"/bin/su",
"/Applications/Cydia.app",
"/Applications/RockApp.app",
"/Applications/Icy.app",
"/usr/sbin/sshd",
"/usr/bin/sshd",
"/usr/libexec/sftp-server",
"/Applications/WinterBoard.app",
"/Applications/SBSettings.app",
"/Applications/MxTube.app",
"/Applications/IntelliScreen.app",
"/Library/MobileSubstrate/DynamicLibraries/Veency.plist",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/Applications/FakeCarrier.app",
"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
"/Applications/blackra1n.app",
"/private/var/stash",
"/private/var/mobile/Library/SBSettings/Themes",
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
"/private/var/tmp/cydia.log",
"/private/var/lib/cydia",
"/var/mobile/Media/.evasi0n7_installed"];
var funcs=[];
var paths=[
"/Applications/blackra1n.app",
"/Applications/Cydia.app",
"/Applications/FakeCarrier.app",
"/Applications/Icy.app",
"/Applications/IntelliScreen.app",
"/Applications/MxTube.app",
"/Applications/RockApp.app",
"/Applications/SBSetttings.app",
"/Applications/WinterBoard.app",
"/bin/bash",
"/bin/sh",
"/bin/su",
"/etc/apt",
"/etc/ssh/sshd_config",
"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
"/Library/MobileSubstrate/DynamicLibraries/Veency.plist",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/pguntether",
"/private/var/lib/cydia",
"/private/var/mobile/Library/SBSettings/Themes",
"/private/var/stash",
"/private/var/tmp/cydia.log",
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
"/usr/bin/cycript",
"/usr/bin/ssh",
"/usr/bin/sshd",
"/usr/libexec/sftp-server",
"/usr/libexec/ssh-keysign",
"/usr/sbin/frida-server",
"/usr/sbin/sshd",
"/var/cache/apt",
"/var/lib/cydia",
"/var/log/syslog",
"/var/mobile/Media/.evasi0n7_installed",
"/var/tmp/cydia.log"];
var libs = [
"CYListenServer",
Expand All @@ -52,6 +62,21 @@ class Module(FridaScript):
"CYObjectiveC",
"frida_agent_main"];
Interceptor.attach(ObjC.classes.NSFileManager["- fileExistsAtPath:"].implementation, {
onEnter: function (args) {
this.path_to_hide = false;
this.path = ObjC.Object(args[2]).toString();
if (paths.indexOf(this.path) >= 0) {
this.path_to_hide = true;
send("Hooking fileExistsAtPath to return false");
}
},
onLeave: function (retval) {
if (this.path_to_hide) {
retval.replace(0x0);
}
}
});
var resolver = new ApiResolver('objc');
resolver.enumerateMatches('*[* is*ailbroken]', {
Expand All @@ -64,7 +89,7 @@ class Module(FridaScript):
send("Hooking: " + func +" to return false");
},
onLeave: function (retval) {
retval.replace(0);
retval.replace(0x0);
}
});
},
Expand Down Expand Up @@ -124,6 +149,12 @@ class Module(FridaScript):
},
});
var f = Module.findExportByName("libSystem.B.dylib","fork");
Interceptor.attach(f, {
onLeave: function (retval) {
retval.replace(0x0);
},
});
'''

# ==================================================================================================================
Expand Down
37 changes: 24 additions & 13 deletions needle/modules/storage/data/keychain_dump_frida.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ class Module(FridaScript):
JS = '''\
if (ObjC.available) {
function bytesToHex(bytes) {
for (var hex = [], i = 0; i < bytes.length; i++) {
hex.push((bytes[i] >>> 4).toString(16));
hex.push((bytes[i] & 0xF).toString(16));
}
return hex.join("");
}
function StringFromNSStringUTF8(nsstring){
var bytearray = nsstring.dataUsingEncoding_(4);
return Memory.readUtf8String(bytearray.bytes(),bytearray.length());
Expand Down Expand Up @@ -116,15 +124,16 @@ class Module(FridaScript):
for (var i = 0; i < result.count(); i++){
var entry = result.objectAtIndex_(i);
send(JSON.stringify({
Data: ObjC.classes.NSString.stringWithUTF8String_(entry.objectForKey_("v_Data").bytes()).valueOf(),
EntitlementGroup: entry.objectForKey_("agrp").valueOf(),
Protection: constants[entry.objectForKey_("pdmn")].valueOf(),
AccessControls: decodeACL(entry),
CreationTime: entry.objectForKey_("cdat").valueOf(),
Account: entry.objectForKey_("acct").valueOf(),
Service: entry.objectForKey_("svce").valueOf(),
ModifiedTime: entry.objectForKey_("mdat").valueOf(),
kSecClass: constants[secItemClasses[secItemClassIter]]
Data: bytesToHex(Memory.readByteArray(entry.objectForKey_("v_Data").bytes(),entry.objectForKey_("v_Data").length())) +
( ObjC.classes.NSString.stringWithUTF8String_(entry.objectForKey_("v_Data").bytes()) ? " (UTF8 String: '" + ObjC.classes.NSString.stringWithUTF8String_(entry.objectForKey_("v_Data").bytes()).valueOf() + "')": "" ),
EntitlementGroup: entry.objectForKey_("agrp").valueOf(),
Protection: constants[entry.objectForKey_("pdmn")].valueOf(),
AccessControls: decodeACL(entry),
CreationTime: entry.objectForKey_("cdat").valueOf(),
Account: ( entry.objectForKey_("acct") ? entry.objectForKey_("acct").valueOf() : "null"),
Service: ( entry.objectForKey_("svce") ? entry.objectForKey_("svce").valueOf() : "null"),
ModifiedTime: entry.objectForKey_("mdat").valueOf(),
kSecClass: constants[secItemClasses[secItemClassIter]]
}));
}
}
Expand Down Expand Up @@ -158,7 +167,9 @@ def module_run(self):
self.printer.warning(e)

def module_post(self):
self.printer.info("Keychain Items:")
self.print_cmd_output()
self.add_issue('Keychain items detected ({})'.format(len(self.results)), None, 'INVESTIGATE', self.options['output'])

if self.results:
self.printer.info("Keychain Items:")
self.print_cmd_output()
self.add_issue('Keychain items detected ({})'.format(len(self.results)), None, 'INVESTIGATE', self.options['output'])
else:
self.printer.warning("No items found.")

0 comments on commit ea5a305

Please sign in to comment.