Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions agent/src/ios/nsuserdefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,38 @@ import {
export const get = (): NSUserDefaults | any => {
// -- Sample Objective-C
//
// NSUserDefaults *d = [[NSUserDefaults alloc] init];
// NSUserDefaults *d = [NSUserDefaults standardUserDefaults];
// NSLog(@"%@", [d dictionaryRepresentation]);

const defaults: NSUserDefaults = ObjC.classes.NSUserDefaults;
const data: NSDictionary = defaults.alloc().init().dictionaryRepresentation();
const defaults: NSUserDefaults = ObjC.classes.NSUserDefaults.standardUserDefaults();
const data: NSDictionary = defaults.dictionaryRepresentation();

return data.toString();
};

export const set = (key: string, value: any, valueType?: string): boolean => {
// -- Sample Objective-C
//
// NSUserDefaults *d = [NSUserDefaults standardUserDefaults];
// [d setObject:value forKey:key];
// [d synchronize];

const defaults: NSUserDefaults = ObjC.classes.NSUserDefaults.standardUserDefaults();

// Determine type and set accordingly
if (valueType === "bool") {
defaults.setBool_forKey_(value, key);
} else if (valueType === "int") {
defaults.setInteger_forKey_(value, key);
} else if (valueType === "float") {
defaults.setDouble_forKey_(value, key);
} else {
// Default to string/object
defaults.setObject_forKey_(value, key);
}

// Persist to disk
defaults.synchronize();

return true;
};
1 change: 1 addition & 0 deletions agent/src/rpc/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ export const ios = {

// ios nsuserdefaults
iosNsuserDefaultsGet: (): NSUserDefaults | any => nsuserdefaults.get(),
iosNsuserDefaultsSet: (key: string, value: any, valueType?: string): boolean => nsuserdefaults.set(key, value, valueType),
};
87 changes: 87 additions & 0 deletions objection/commands/ios/nsuserdefaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
from objection.state.connection import state_connection


def _get_flag_value(args: list, flag: str) -> str:
"""
Returns the value for a flag.

:param args:
:param flag:
:return:
"""

return args[args.index(flag) + 1] if flag in args else None


def get(args: list = None) -> None:
"""
Gets all of the values stored in NSUserDefaults and prints
Expand All @@ -16,3 +28,78 @@ def get(args: list = None) -> None:
defaults = api.ios_nsuser_defaults_get()

click.secho(defaults, bold=True)


def set(args: list = None) -> None:
"""
Sets a value in NSUserDefaults.

:param args:
:return:
"""

if not args or len(args) < 2:
click.secho('Usage: ios nsuserdefaults set <key> <value> [--type string|int|float|bool]', fg='red')
return

# Get explicit type if provided
value_type = _get_flag_value(args, '--type')

# Remove --type and its value from args if present
if '--type' in args:
type_index = args.index('--type')
args = args[:type_index] + args[type_index + 2:]

if len(args) < 2:
click.secho('Usage: ios nsuserdefaults set <key> <value> [--type string|int|float|bool]', fg='red')
return

key = args[0]
value_str = args[1]

# Parse value based on type
if value_type == 'bool':
value = value_str.lower() in ['true', '1', 'yes']
elif value_type == 'int':
try:
value = int(value_str)
except ValueError:
click.secho(f'Invalid integer value: {value_str}', fg='red')
return
elif value_type == 'float':
try:
value = float(value_str)
except ValueError:
click.secho(f'Invalid float value: {value_str}', fg='red')
return
else:
# Default to string, but try to auto-detect type
if not value_type:
if value_str.lower() in ['true', 'false']:
value_type = 'bool'
value = value_str.lower() == 'true'
elif value_str.isdigit() or (value_str.startswith('-') and value_str[1:].isdigit()):
value_type = 'int'
value = int(value_str)
elif '.' in value_str:
try:
value = float(value_str)
value_type = 'float'
except ValueError:
value = value_str
value_type = 'string'
else:
value = value_str
value_type = 'string'
else:
value = value_str

click.secho(f'Setting NSUserDefaults key: {key} = {value} (type: {value_type})', dim=True)

api = state_connection.get_api()
result = api.ios_nsuser_defaults_set(key, value, value_type)

if result:
click.secho(f'Successfully set {key}', fg='green')
else:
click.secho(f'Failed to set {key}', fg='red')
4 changes: 4 additions & 0 deletions objection/console/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,10 @@
'get': {
'meta': 'Get all of the entries',
'exec': nsuserdefaults.get
},
'set': {
'meta': 'Set a value for a key',
'exec': nsuserdefaults.set
}
}
},
Expand Down
25 changes: 25 additions & 0 deletions objection/console/helpfiles/ios.nsuserdefaults.set.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Command: ios nsuserdefaults set

Usage: ios nsuserdefaults set <key> <value> [--type string|int|float|bool]

Sets a value in the application's NSUserDefaults for the specified key.
The command will attempt to auto-detect the value type based on the input,
but you can explicitly specify the type using the --type flag.

Type Detection:
- "true" or "false" -> boolean
- Numbers without decimal -> integer
- Numbers with decimal -> float
- Everything else -> string

Arguments:
<key> The NSUserDefaults key to set
<value> The value to store
--type Optional: Explicitly specify the value type (string|int|float|bool)

Examples:
ios nsuserdefaults set username "john.doe"
ios nsuserdefaults set isFirstLaunch false
ios nsuserdefaults set loginAttempts 3
ios nsuserdefaults set apiVersion 2.5
ios nsuserdefaults set debugMode true --type bool
83 changes: 68 additions & 15 deletions objection/console/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ..__init__ import __version__
from ..state.app import app_state
from ..state.connection import state_connection
from ..utils.agent import Agent, AgentConfig
from ..utils.helpers import get_tokens


Expand Down Expand Up @@ -279,6 +280,59 @@ def _find_command_help(self, tokens: list) -> str:

return user_help

@staticmethod
def perform_reconnect() -> bool:
"""
Performs the actual reconnection logic.

:return: True if successful, False otherwise
"""
try:
# Get current connection config
current_agent = state_connection.agent

# Cleanup current agent (ignore errors if already destroyed)
click.secho('Unloading current agent...', dim=True)
try:
if current_agent.script:
current_agent.script.unload()
except (frida.InvalidOperationError, Exception):
pass # Script already destroyed or detached

try:
if current_agent.session:
current_agent.session.detach()
except (frida.InvalidOperationError, Exception):
pass # Session already detached

# Create new agent with same config
click.secho('Creating new agent session...', dim=True)
new_agent = Agent(AgentConfig(
name=state_connection.name,
host=state_connection.host,
port=state_connection.port,
device_type=state_connection.device_type,
device_id=state_connection.device_id,
spawn=False, # Don't spawn on reconnect, attach to existing
foremost=state_connection.foremost,
debugger=state_connection.debugger,
pause=not state_connection.no_pause,
uid=state_connection.uid
))

new_agent.run()
state_connection.set_agent(new_agent)

click.secho('Successfully reconnected!', fg='green')
return True

except (frida.ServerNotRunningError, frida.TimedOutError) as e:
click.secho('Failed to reconnect with error: {0}'.format(e), fg='red')
return False
except Exception as e:
click.secho('Failed to reconnect: {0}'.format(e), fg='red')
return False

@staticmethod
def handle_reconnect(document: str) -> bool:
"""
Expand All @@ -292,22 +346,8 @@ def handle_reconnect(document: str) -> bool:
"""

if document.strip() in ('reconnect', 'reset'):

click.secho('Reconnecting...', dim=True)

try:
# TODO
# state_connection.a.unload()
#
# agent = OldAgent()
# agent.inject()
# state_connection.a = agent

click.secho('Not yet implemented!', fg='yellow')

except (frida.ServerNotRunningError, frida.TimedOutError) as e:
click.secho('Failed to reconnect with error: {0}'.format(e), fg='red')

Repl.perform_reconnect()
return True

return False
Expand Down Expand Up @@ -361,6 +401,19 @@ def run(self, quiet: bool) -> None:
# find something to run
self.run_command(document)

except frida.InvalidOperationError as e:
# Check if script was destroyed - attempt auto-reconnect
if 'script has been destroyed' in str(e).lower() or 'script is destroyed' in str(e).lower():
click.secho('Script has been destroyed. Attempting auto-reconnect...', fg='yellow')
if self.perform_reconnect():
click.secho('Reconnected! Please retry your command.', fg='green')
else:
click.secho('Auto-reconnect failed. Use "reconnect" to try again manually.', fg='red')
else:
click.secho('A Frida operation error has occurred.', fg='red', bold=True)
click.secho('{0}'.format(e), fg='red')
click.secho('\nPython stack trace: {}'.format(traceback.format_exc()), dim=True)

except frida.core.RPCException as e:
click.secho('A Frida agent exception has occurred.', fg='red', bold=True)
click.secho('{0}'.format(e), fg='red')
Expand Down
56 changes: 55 additions & 1 deletion tests/commands/ios/test_nsuserdefaults.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest
from unittest import mock

from objection.commands.ios.nsuserdefaults import get
from objection.commands.ios.nsuserdefaults import get, set
from ...helpers import capture


Expand All @@ -14,3 +14,57 @@ def test_get(self, mock_api):
output = o

self.assertEqual(output, 'foo\n')

@mock.patch('objection.state.connection.state_connection.get_api')
def test_set_string(self, mock_api):
mock_api.return_value.ios_nsuser_defaults_set.return_value = True

with capture(set, ['testKey', 'testValue']) as o:
output = o

self.assertIn('Successfully set testKey', output)
mock_api.return_value.ios_nsuser_defaults_set.assert_called_once_with('testKey', 'testValue', 'string')

@mock.patch('objection.state.connection.state_connection.get_api')
def test_set_bool(self, mock_api):
mock_api.return_value.ios_nsuser_defaults_set.return_value = True

with capture(set, ['isEnabled', 'true']) as o:
output = o

self.assertIn('Successfully set isEnabled', output)
mock_api.return_value.ios_nsuser_defaults_set.assert_called_once_with('isEnabled', True, 'bool')

@mock.patch('objection.state.connection.state_connection.get_api')
def test_set_int(self, mock_api):
mock_api.return_value.ios_nsuser_defaults_set.return_value = True

with capture(set, ['count', '42']) as o:
output = o

self.assertIn('Successfully set count', output)
mock_api.return_value.ios_nsuser_defaults_set.assert_called_once_with('count', 42, 'int')

@mock.patch('objection.state.connection.state_connection.get_api')
def test_set_with_explicit_type(self, mock_api):
mock_api.return_value.ios_nsuser_defaults_set.return_value = True

with capture(set, ['version', '2.5', '--type', 'float']) as o:
output = o

self.assertIn('Successfully set version', output)
mock_api.return_value.ios_nsuser_defaults_set.assert_called_once_with('version', 2.5, 'float')

@mock.patch('objection.state.connection.state_connection.get_api')
def test_set_missing_arguments(self, mock_api):
with capture(set, ['onlyKey']) as o:
output = o

self.assertIn('Usage:', output)

@mock.patch('objection.state.connection.state_connection.get_api')
def test_set_no_arguments(self, mock_api):
with capture(set, []) as o:
output = o

self.assertIn('Usage:', output)