diff --git a/tests/state/__init__.py b/tests/state/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/state/test_app.py b/tests/state/test_app.py new file mode 100644 index 00000000..3c503b34 --- /dev/null +++ b/tests/state/test_app.py @@ -0,0 +1,8 @@ +import unittest + +from objection.state.app import app_state + + +class TestApp(unittest.TestCase): + def test_app_should_not_debug_hooks_by_default(self): + self.assertFalse(app_state.should_debug_hooks()) diff --git a/tests/state/test_connection.py b/tests/state/test_connection.py new file mode 100644 index 00000000..c4ccdfd2 --- /dev/null +++ b/tests/state/test_connection.py @@ -0,0 +1,34 @@ +import unittest + +from objection.state.connection import state_connection + + +class TestConnection(unittest.TestCase): + def setUp(self): + pass + + def test_default_type_is_usb(self): + comms_type = state_connection.get_comms_type() + comms_type_string = state_connection.get_comms_type_string() + + self.assertEqual(comms_type, 0) + self.assertEqual(comms_type_string, 'usb') + + def test_sets_type_to_network(self): + state_connection.use_network() + + comms_type = state_connection.get_comms_type() + comms_type_string = state_connection.get_comms_type_string() + + self.assertEqual(comms_type, 1) + self.assertEqual(comms_type_string, 'net') + + def test_sets_type_usb_after_setting_type_network(self): + state_connection.use_network() + state_connection.use_usb() + + comms_type = state_connection.get_comms_type() + comms_type_string = state_connection.get_comms_type_string() + + self.assertEqual(comms_type, 0) + self.assertEqual(comms_type_string, 'usb') diff --git a/tests/state/test_device.py b/tests/state/test_device.py new file mode 100644 index 00000000..773e15b5 --- /dev/null +++ b/tests/state/test_device.py @@ -0,0 +1,11 @@ +import unittest + +from objection.state.device import device_state + + +class TestDevice(unittest.TestCase): + def test_device_representation(self): + device_state.device_type = 'ios' + device_state.frida_version = '10.6.1' + + self.assertEqual(repr(device_state), '') diff --git a/tests/state/test_jobs.py b/tests/state/test_jobs.py new file mode 100644 index 00000000..8049aa23 --- /dev/null +++ b/tests/state/test_jobs.py @@ -0,0 +1,25 @@ +import unittest + +from objection.state.jobs import job_manager_state + + +class TestJobManager(unittest.TestCase): + def tearDown(self): + job_manager_state.jobs = [] + + def test_job_manager_starts_with_empty_jobs(self): + self.assertEqual(len(job_manager_state.jobs), 0) + + def test_adds_jobs(self): + job_manager_state.add_job('foo') + + self.assertEqual(len(job_manager_state.jobs), 1) + + def test_removes_jobs(self): + job_manager_state.add_job('foo') + job_manager_state.add_job('bar') + + job_manager_state.remove_job('foo') + job_manager_state.remove_job('bar') + + self.assertEqual(len(job_manager_state.jobs), 0) diff --git a/tests/state/test_sqlite.py b/tests/state/test_sqlite.py new file mode 100644 index 00000000..b24c32e7 --- /dev/null +++ b/tests/state/test_sqlite.py @@ -0,0 +1,56 @@ +import unittest +from unittest import mock + +from objection.state.sqlite import sqlite_manager_state +from ..helpers import capture + + +class TestSQLite(unittest.TestCase): + def tearDown(self): + sqlite_manager_state.file = sqlite_manager_state.temp_file = None + + def test_reports_not_connected_by_default(self): + status = sqlite_manager_state.is_connected() + + self.assertFalse(status) + + def test_reports_connected_with_file_and_tempfile_set(self): + sqlite_manager_state.file = 'foo' + sqlite_manager_state.temp_file = 'bar' + + status = sqlite_manager_state.is_connected() + + self.assertTrue(status) + + @mock.patch('objection.state.sqlite.tempfile') + def test_gets_new_cache_directory_for_temp_storage(self, mock_tempfile): + mock_tempfile.mkstemp.return_value = 1, '/tmp/foo' + + directory = sqlite_manager_state.get_cache_dir() + + self.assertEqual(directory, '/tmp/foo') + + def test_gets_existing_temp_directory_for_temp_storage(self): + sqlite_manager_state.temp_file = '/foo/bar' + + directory = sqlite_manager_state.get_cache_dir() + + self.assertEqual(directory, '/foo/bar') + + @mock.patch('objection.state.sqlite.os') + def test_will_cleanup_when_connected(self, mock_os): + mock_os.remove.return_value = None + + sqlite_manager_state.file = 'foo' + sqlite_manager_state.temp_file = 'bar' + + with capture(sqlite_manager_state.cleanup) as o: + output = o + + self.assertEqual(output, '[sqlite manager] Removing cached copy of SQLite database: foo at bar\n') + self.assertIsNone(sqlite_manager_state.file) + self.assertIsNone(sqlite_manager_state.temp_file) + self.assertIsNone(sqlite_manager_state.full_remote_file) + + def test_representation(self): + self.assertEqual(repr(sqlite_manager_state), '') diff --git a/tests/utils/patchers/__init__.py b/tests/utils/patchers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/patchers/test_android.py b/tests/utils/patchers/test_android.py new file mode 100644 index 00000000..0a559996 --- /dev/null +++ b/tests/utils/patchers/test_android.py @@ -0,0 +1,137 @@ +import os +import unittest +from unittest import mock + +from objection.utils.patchers.android import AndroidGadget, AndroidPatcher + + +class TestAndroidGadget(unittest.TestCase): + @mock.patch('objection.utils.patchers.android.Github') + @mock.patch('objection.utils.patchers.android.os') + def setUp(self, github, mock_os): + mock_os.path.exists.return_value = True + + self.android_gadget = AndroidGadget(github) + + self.github_get_assets_sample = [ + { + "url": "https://api.github.com/repos/frida/frida/releases/assets/5005221", + "id": 5005221, + "name": "frida-gadget-10.6.8-android-x86.so.xz", + "label": "", + "uploader": { + "id": 735197, + }, + "state": "uploaded", + "size": 12912624, + "download_count": 1, + "created_at": "2017-10-07T00:01:10Z", + "updated_at": "2017-10-07T00:01:17Z", + "browser_download_url": "https://github.com/frida/frida/releases/download/" + "10.6.8/frida-gadget-10.6.8-android-x86.so.xz" + } + ] + + def test_sets_architecture(self): + self.android_gadget.set_architecture('x86') + self.assertEqual(self.android_gadget.architecture, 'x86') + + def test_raises_exception_with_invalid_architecture(self): + with self.assertRaises(Exception) as _: + self.android_gadget.set_architecture('foo') + + def test_sets_architecture_and_returns_context(self): + result = self.android_gadget.set_architecture('x86') + self.assertEqual(type(result), AndroidGadget) + + def test_gets_architecture_when_set(self): + self.android_gadget.set_architecture('x86') + architecture = self.android_gadget.get_architecture() + + self.assertEqual(architecture, 'x86') + + def test_gets_frida_library_path(self): + self.android_gadget.set_architecture('x86') + + frida_path = self.android_gadget.get_frida_library_path() + self.assertTrue('.objection/android/x86/libfrida-gadget.so' in frida_path) + + def test_fails_to_get_frida_library_path_without_architecture(self): + with self.assertRaises(Exception) as _: + self.android_gadget.get_frida_library_path() + + @mock.patch('objection.utils.patchers.android.os') + def test_checks_if_gadget_exists_if_it_really_exists(self, mock_os): + mock_os.path.exists.return_value = True + self.android_gadget.set_architecture('x86') + + status = self.android_gadget.gadget_exists() + + self.assertTrue(status) + + @mock.patch('objection.utils.patchers.android.os') + def test_checks_if_gadget_exists_if_it_really_does_not_exist(self, mock_os): + mock_os.path.exists.return_value = False + self.android_gadget.set_architecture('x86') + + status = self.android_gadget.gadget_exists() + + self.assertFalse(status) + + def test_check_if_gadget_exists_fails_without_architecture(self): + with self.assertRaises(Exception) as _: + self.android_gadget.gadget_exists() + + def test_can_find_download_url_for_gadget(self): + mock_github = mock.MagicMock() + mock_github.get_assets.return_value = self.github_get_assets_sample + + self.android_gadget.github = mock_github + self.android_gadget.architecture = 'x86' + + # the method we actually testing here! + url = self.android_gadget._get_download_url() + + self.assertEqual(url, 'https://github.com/frida/frida/releases/download/' + '10.6.8/frida-gadget-10.6.8-android-x86.so.xz') + + def test_throws_exception_when_download_url_could_not_be_determined(self): + mock_github = mock.MagicMock() + mock_github.get_assets.return_value = self.github_get_assets_sample + + self.android_gadget.github = mock_github + self.android_gadget.architecture = 'arm' + + # the method we actually testing here! + with self.assertRaises(Exception) as _: + self.android_gadget._get_download_url() + + +class TestAndroidPatcher(unittest.TestCase): + @mock.patch('objection.utils.patchers.android.BasePlatformPatcher.__init__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.android.AndroidPatcher.__del__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.android.tempfile') + def test_inits_patcher(self, tempfile): + tempfile.mkdtemp.return_value = '/tmp/test' + + patcher = AndroidPatcher() + + self.assertIsNone(patcher.apk_source) + self.assertEqual(patcher.apk_temp_directory, '/tmp/test') + self.assertEqual(patcher.apk_temp_frida_patched, '/tmp/test.objection.apk') + self.assertFalse(patcher.skip_cleanup) + self.assertTrue('objection/utils/patchers/../assets/objection.jks' in patcher.keystore) + self.assertTrue(os.path.exists(patcher.keystore)) + + @mock.patch('objection.utils.patchers.android.AndroidPatcher.__init__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.android.AndroidPatcher.__del__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.android.tempfile') + @mock.patch('objection.utils.patchers.android.os') + def test_set_android_apk_source(self, _, mock_os): + mock_os.path.exists.return_value = True + patcher = AndroidPatcher() + + source = patcher.set_apk_source('foo.apk') + + self.assertEqual(type(source), AndroidPatcher) + self.assertEqual(patcher.apk_source, 'foo.apk') diff --git a/tests/utils/patchers/test_base.py b/tests/utils/patchers/test_base.py new file mode 100644 index 00000000..913e0154 --- /dev/null +++ b/tests/utils/patchers/test_base.py @@ -0,0 +1,77 @@ +import unittest +from unittest import mock + +from objection.utils.patchers.base import BasePlatformGadget, BasePlatformPatcher +from ...helpers import capture + + +class TestBasePlatformGadget(unittest.TestCase): + @mock.patch('objection.utils.patchers.base.Github') + def setUp(self, mock_github): + self.gadget = BasePlatformGadget(github=mock_github) + + @mock.patch('objection.utils.patchers.base.os') + def test_sets_version_to_zero_if_no_local_record_is_found(self, mock_os): + mock_os.path.exists.return_value = False + version = self.gadget.get_local_version('test') + + self.assertEqual(version, '0') + + +class TestBasePlatformPatcher(unittest.TestCase): + def setUp(self): + pass + + @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=True)) + def test_inits_base_patcher(self): + base_patcher = BasePlatformPatcher() + + self.assertTrue(base_patcher.have_all_commands) + self.assertEqual(base_patcher.command_run_timeout, 300) + + @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=True)) + def test_are_requirements_met_returns_true_if_met(self): + base_patcher = BasePlatformPatcher() + + self.assertTrue(base_patcher.are_requirements_met()) + + @mock.patch('objection.utils.patchers.base.BasePlatformPatcher._check_commands', mock.Mock(return_value=False)) + def test_are_requirements_met_returns_false_if_not_met(self): + base_patcher = BasePlatformPatcher() + + self.assertFalse(base_patcher.are_requirements_met()) + + @mock.patch('objection.utils.patchers.base.BasePlatformPatcher.__init__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.base.shutil') + def test_check_commands_finds_commands_and_sets_location(self, mock_shutil): + mock_shutil.which.return_value = '/bin/test' + + base_patcher = BasePlatformPatcher() + base_patcher.required_commands = { + 'aapt': { + 'installation': 'apt install aapt (Kali Linux)' + } + } + + check_result = base_patcher._check_commands() + + self.assertTrue(check_result) + self.assertEqual(base_patcher.required_commands['aapt']['location'], '/bin/test') + + @mock.patch('objection.utils.patchers.base.BasePlatformPatcher.__init__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.base.shutil') + def test_check_commands_fails_to_find_command_and_displays_error(self, mock_shutil): + mock_shutil.which.return_value = None + + base_patcher = BasePlatformPatcher() + base_patcher.required_commands = { + 'aapt': { + 'installation': 'apt install aapt (Kali Linux)' + } + } + + with capture(base_patcher._check_commands) as o: + output = o + + self.assertEqual(output, 'Unable to find aapt. Install it with:' + ' apt install aapt (Kali Linux) before continuing.\n') diff --git a/tests/utils/patchers/test_github.py b/tests/utils/patchers/test_github.py new file mode 100644 index 00000000..1ffc5bd8 --- /dev/null +++ b/tests/utils/patchers/test_github.py @@ -0,0 +1,88 @@ +import unittest +from unittest import mock + +from objection.utils.patchers.github import Github + + +class TestGithub(unittest.TestCase): + def setUp(self): + self.github = Github() + self.mock_response = { + "tag_name": "10.6.9", + "target_commitish": "master", + "name": "Frida 10.6.9", + "created_at": "2017-10-09T23:52:02Z", + "published_at": "2017-10-10T00:02:48Z", + "assets": [ + { + "url": "https://api.github.com/repos/frida/frida/releases/assets/5024320", + "name": "frida-core-devkit-10.6.9-android-arm.tar.xz", + "label": "", + "updated_at": "2017-10-10T00:13:36Z", + "browser_download_url": "https://github.com/frida/frida/releases/download/" + "10.6.9/frida-core-devkit-10.6.9-android-arm.tar.xz" + }, + ], + "tarball_url": "https://api.github.com/repos/frida/frida/tarball/10.6.9", + "zipball_url": "https://api.github.com/repos/frida/frida/zipball/10.6.9", + "body": "See http://www.frida.re/news/ for details." + } + + @mock.patch('objection.utils.patchers.github.requests') + def test_makes_call_and_stores_result_in_cache(self, mock_requests): + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = self.mock_response + + mock_requests.get.return_value = mock_response + + result = self.github._call('/test') + + self.assertEqual(result, self.mock_response) + self.assertEqual(len(self.github.request_cache), 1) + + @mock.patch('objection.utils.patchers.github.requests') + def test_makes_call_and_stores_result_in_cache_and_fetches_next_from_cache(self, mock_requests): + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = self.mock_response + + mock_requests.get.return_value = mock_response + + self.github._call('/test') + + # entry is now stored in cache, update the next response object + # and make the request again. + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'other'} + + mock_requests.get.return_value = mock_response + + result = self.github._call('/test') + + self.assertEqual(result, self.mock_response) + + @mock.patch('objection.utils.patchers.github.requests') + def test_makes_call_and_gets_latest_version(self, mock_requests): + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = self.mock_response + + mock_requests.get.return_value = mock_response + + result = self.github.get_latest_version() + + self.assertEqual(result, self.mock_response['tag_name']) + + @mock.patch('objection.utils.patchers.github.requests') + def test_makes_call_and_gets_assets(self, mock_requests): + mock_response = mock.Mock() + mock_response.status_code = 200 + mock_response.json.return_value = self.mock_response + + mock_requests.get.return_value = mock_response + + result = self.github.get_assets() + + self.assertEqual(result, self.mock_response['assets']) diff --git a/tests/utils/patchers/test_ios.py b/tests/utils/patchers/test_ios.py new file mode 100644 index 00000000..6b2f8e5f --- /dev/null +++ b/tests/utils/patchers/test_ios.py @@ -0,0 +1,68 @@ +import unittest +from unittest import mock + +from objection.utils.patchers.ios import IosGadget, IosPatcher + + +class TestIosGadget(unittest.TestCase): + @mock.patch('objection.utils.patchers.ios.Github') + @mock.patch('objection.utils.patchers.android.os') + def setUp(self, mock_github, mock_os): + mock_os.path.exists.return_value = True + + self.ios_gadget = IosGadget(github=mock_github) + + self.github_get_assets_sample = [ + { + "url": "https://api.github.com/repos/frida/frida/releases/assets/5005221", + "id": 5005221, + "name": "frida-gadget-10.6.8-ios-universal.dylib.xz", + "label": "", + "uploader": { + "id": 735197, + }, + "state": "uploaded", + "size": 12912624, + "download_count": 1, + "created_at": "2017-10-07T00:01:10Z", + "updated_at": "2017-10-07T00:01:17Z", + "browser_download_url": "https://github.com/frida/frida/releases/download/" + "frida-gadget-10.6.8-ios-universal.dylib.xz" + } + ] + + def test_gets_gadget_path(self): + self.ios_gadget.ios_dylib_gadget_path = '/tmp/foo' + + result = self.ios_gadget.get_gadget_path() + + self.assertEqual(result, '/tmp/foo') + + @mock.patch('objection.utils.patchers.ios.os') + def test_checks_if_gadget_exists(self, mock_os): + mock_os.path.exists.return_value = True + + result = self.ios_gadget.gadget_exists() + + self.assertTrue(result) + + def test_can_find_asset_download_url(self): + mock_github = mock.MagicMock() + mock_github.get_assets.return_value = self.github_get_assets_sample + + self.ios_gadget.github = mock_github + + result = self.ios_gadget._get_download_url() + + self.assertEqual(result, 'https://github.com/frida/frida/releases/download/' + 'frida-gadget-10.6.8-ios-universal.dylib.xz') + + +class TestIosPatcher(unittest.TestCase): + @mock.patch('objection.utils.patchers.ios.IosPatcher.__init__', mock.Mock(return_value=None)) + @mock.patch('objection.utils.patchers.ios.IosPatcher.__del__', mock.Mock(return_value=None)) + def test_sets_provisioning_profile(self): + patcher = IosPatcher() + patcher.set_provsioning_profile('profile.mobileprovision') + + self.assertEqual(patcher.provision_file, 'profile.mobileprovision') diff --git a/tests/utils/test_frida_transport.py b/tests/utils/test_frida_transport.py new file mode 100644 index 00000000..9bc8b7db --- /dev/null +++ b/tests/utils/test_frida_transport.py @@ -0,0 +1,294 @@ +import unittest +import uuid +from unittest import mock + +from objection.utils.frida_transport import RunnerMessage, FridaJobRunner, FridaRunner +from ..helpers import capture + + +class TestRunnerMessage(unittest.TestCase): + def setUp(self): + self.success_message_sample = { + 'status': 'success', + 'error_reason': None, + 'type': 'send', + 'data': { + 'foo': 'bar' + } + } + + self.failed_message_sample = { + 'status': 'error', + 'error_reason': 'testing', + 'type': 'send', + 'data': None + } + + def test_inits_with_message_and_extra_data(self): + message = RunnerMessage(self.success_message_sample, {'baz': 'bar'}) + + self.assertTrue(message.success) + self.assertEqual(message.type, 'send') + self.assertIsNone(message.error_reason) + self.assertEqual(message.data, {'foo': 'bar'}) + self.assertEqual(message.extra_data, {'baz': 'bar'}) + + def test_inits_with_message_and_empty_extra_data(self): + message = RunnerMessage(self.success_message_sample, None) + + self.assertIsNone(message.extra_data) + + def test_reports_successful(self): + message = RunnerMessage(self.success_message_sample, None) + + self.assertTrue(message.is_successful()) + + def test_reports_failed(self): + message = RunnerMessage(self.failed_message_sample, None) + + self.assertFalse(message.is_successful()) + + def test_gets_extra_data_with_helper_method(self): + message = RunnerMessage(self.success_message_sample, 'test') + + data = message.get_extra_data() + + self.assertEqual(data, 'test') + + def test_gets_extra_data_with_helper_with_no_data(self): + message = RunnerMessage(self.success_message_sample, None) + + data = message.get_extra_data() + + self.assertEqual(data, None) + + def test_gets_data_from_data_dictionary_by_dot_notation(self): + message = RunnerMessage(self.success_message_sample, {'foo': 'bar'}) + + data = message.foo + + self.assertEqual(data, 'bar') + + def test_gets_data_from_dictionary_by_key_notation(self): + message = RunnerMessage(self.success_message_sample, {'foo': 'bar'}) + + data = message['foo'] + + self.assertEqual(data, 'bar') + + def test_prints_representation_of_successful_message(self): + message = RunnerMessage(self.success_message_sample, None) + + self.assertEqual(repr(message), '') + + def test_prints_representation_of_failed_message(self): + message = RunnerMessage(self.failed_message_sample, None) + + self.assertEqual(repr(message), '') + + +class TestFridaJobRunner(unittest.TestCase): + def setUp(self): + self.runner = FridaJobRunner(name='testing') + + # set status values for some determined by + # uuid & time + self.runner.id = 'testing-id' + self.runner.started = '2017-10-09 15:03:23' + + self.successful_message = { + 'payload': { + 'type': 'send', + 'status': 'success', + 'data': 'data for unittest' + } + } + + self.error_message = { + 'payload': { + 'type': 'send', + 'status': 'error', + 'data': 'error data for unittest' + } + } + + self.unknown_message = { + 'payload': { + 'type': 'send', + 'status': 'invalid', + 'data': 'invalid data for unittest' + } + } + + self.invalid_message = { + 'invalid': 'invalid' + } + + @mock.patch('objection.utils.frida_transport.random.choice') + def test_inits_job_runner(self, choice): + choice.return_value = 'green' + + runner = FridaJobRunner('test') + + self.assertEqual(runner.name, 'test') + self.assertEqual(type(runner.id), uuid.UUID) + self.assertFalse(runner.has_had_error) + self.assertIsNone(runner.hook) + self.assertIsNone(runner.session) + self.assertIsNone(runner.script) + self.assertEqual(runner.success_color, 'green') + + def test_receive_successful_message_from_hook(self): + with capture(self.runner.on_message, self.successful_message, None) as o: + output = o + + self.assertEqual(output, '[testing-id] [send] data for unittest\n') + + def test_receive_error_message_from_hook(self): + with capture(self.runner.on_message, self.error_message, None) as o: + output = o + + expected_output = 'Failed to process an incoming message from hook: \'error_reason\'\n' + + self.assertEqual(output, expected_output) + # self.assertTrue(self.runner.has_had_error) # TODO: Erm, ??? + + def test_receive_unknown_message_from_hook(self): + with capture(self.runner.on_message, self.unknown_message, None) as o: + output = o + + self.assertEqual(output, '[testing-id][invalid] invalid data for unittest\n') + + @mock.patch('objection.utils.frida_transport.app_state.should_debug_hooks') + def test_receive_message_and_debug_response_outut(self, should_debug_hooks): + should_debug_hooks.return_value = True + + with capture(self.runner.on_message, self.successful_message, None) as o: + output = o + + expected_value = """- [response] ------------------ +{ + "payload": { + "type": "send", + "status": "success", + "data": "data for unittest" + } +} +- [./response] ---------------- +[testing-id] [send] data for unittest +""" + + self.assertEqual(output, expected_value) + + def test_prints_representation_of_running_job(self): + self.assertEqual(repr(self.runner), '') + + +class TestFridaRunner(unittest.TestCase): + def setUp(self): + self.runner = FridaRunner() + + self.successful_message = { + 'payload': { + 'type': 'send', + 'status': 'success', + 'error_reason': None, + 'data': 'data for unittest' + } + } + + self.error_message = { + 'payload': { + 'type': 'send', + 'status': 'error', + 'error_reason': 'error_message', + 'data': 'error data for unittest' + } + } + + self.sample_hook = """// this is a comment + +var response = { + status: 'success', + error_reason: NaN, + type: 'file-readable', + data: { path: '{{ path }}', readable: Boolean(file.canRead()) } +}; + +send(response);""" + + def test_init_runner_without_hook(self): + runner = FridaRunner() + + self.assertEqual(runner.messages, []) + self.assertIsNone(runner.script) + + def test_init_runner_with_hook(self): + runner = FridaRunner('test') + + self.assertEqual(runner.hook, 'test') + + def test_handles_incoming_success_message_and_adds_message(self): + self.runner._on_message(self.successful_message, None) + + self.assertEqual(len(self.runner.messages), 1) + + def test_handles_incoming_error_message_and_warns_while_adding_message(self): + with capture(self.runner._on_message, self.error_message, None) as o: + output = o + + expected_output = '[hook failure] error_message\n' + + self.assertEqual(output, expected_output) + self.assertEqual(len(self.runner.messages), 1) + + @mock.patch('objection.utils.frida_transport.app_state.should_debug_hooks') + def test_handles_incoming_success_message_and_prints_debug_output(self, should_debug_hooks): + should_debug_hooks.return_value = True + + with capture(self.runner._on_message, self.successful_message, None) as o: + output = o + + expected_output = """- [response] ------------------ +{ + "payload": { + "type": "send", + "status": "success", + "error_reason": null, + "data": "data for unittest" + } +} +- [./response] ---------------- +""" + + self.assertEqual(output, expected_output) + + def test_hook_processor_beautifies_javascript_output_from_hook_property(self): + self.runner.hook = self.sample_hook + + hook = self.runner._hook_processor() + + expected_outut = """var response = { + status: 'success', + error_reason: NaN, + type: 'file-readable', + data: { path: '', readable: Boolean(file.canRead()) } +}; +send(response);""" + + self.assertEqual(hook, expected_outut) + + def test_can_fetch_last_message_with_multiple_messages_received(self): + # ignore the output we get from the error message + with capture(self.runner._on_message, self.error_message, None) as _: + pass + self.runner._on_message(self.successful_message, None) + + last_message = self.runner.get_last_message() + + self.assertEqual(last_message.data, self.successful_message['payload']['data']) + + def test_sets_hook_with(self): + self.runner.set_hook_with_data('{{ test }}', test='testing123') + + self.assertEqual(self.runner.hook, 'testing123') diff --git a/tests/utils/test_templates.py b/tests/utils/test_templates.py new file mode 100644 index 00000000..07111cf5 --- /dev/null +++ b/tests/utils/test_templates.py @@ -0,0 +1,245 @@ +import unittest + +from objection.utils.templates import _get_name_with_js_suffix, generic_hook, ios_hook, android_hook + + +class TestTemplates(unittest.TestCase): + def test_gets_hook_name_and_adds_js_prefix(self): + result = _get_name_with_js_suffix('foo') + + self.assertEqual(result, 'foo.js') + + def test_gets_hook_name_and_does_not_add_js_prefix_if_exists(self): + result = _get_name_with_js_suffix('foo.js') + + self.assertEqual(result, 'foo.js') + + def test_finds_and_compiles_generic_hooks_with_an_exception_handler(self): + hook = generic_hook('memory/write') + + expected_output = """try { + + // Writes arbitrary bytes to a memory address. + +var bar = eval(['{{ pattern }}']); + +Memory.writeByteArray(ptr('{{ destination }}'), bar); + + +} catch (err) { + + var response = { + status: 'error', + error_reason: err.message, + type: 'global-exception', + data: {} + }; + + send(response); +} +""" + + self.assertEqual(hook, expected_output) + + def test_finds_and_compiles_generic_hooks_without_an_exception_handler(self): + hook = generic_hook('memory/write', skip_trycarch=True) + + expected_output = """// Writes arbitrary bytes to a memory address. + +var bar = eval(['{{ pattern }}']); + +Memory.writeByteArray(ptr('{{ destination }}'), bar); +""" + + self.assertEqual(hook, expected_output) + + def test_finds_and_compiles_ios_hooks_with_an_exception_handler(self): + hook = ios_hook('filesystem/pwd') + + expected_output = """if (ObjC.available) { + + try { + + // Determines the current working directory, based +// on the main bundles path on the iOS device. + +var NSBundle = ObjC.classes.NSBundle; +var BundleURL = NSBundle.mainBundle().bundlePath(); + +var response = { + status: 'success', + error_reason: NaN, + type: 'current-working-directory', + data: { + cwd: String(BundleURL) + } +}; + +send(response); + +// -- Sample Objective-C +// +// NSURL *bundleURL = [[NSBundle mainBundle] bundleURL]; + + + } catch (err) { + + var response = { + status: 'error', + error_reason: err.message, + type: 'global-exception', + data: {} + }; + + send(response); + } + +} else { + + var response = { + status: 'error', + error_reason: 'Objective-C runtime is not available.', + type: 'global-exception', + data: {} + }; + + send(response); +} +""" + + self.assertEqual(hook, expected_output) + + def test_finds_and_compiles_ios_hooks_without_an_exception_handler(self): + hook = ios_hook('filesystem/pwd', skip_trycatch=True) + + expected_output = """// Determines the current working directory, based +// on the main bundles path on the iOS device. + +var NSBundle = ObjC.classes.NSBundle; +var BundleURL = NSBundle.mainBundle().bundlePath(); + +var response = { + status: 'success', + error_reason: NaN, + type: 'current-working-directory', + data: { + cwd: String(BundleURL) + } +}; + +send(response); + +// -- Sample Objective-C +// +// NSURL *bundleURL = [[NSBundle mainBundle] bundleURL]; +""" + + self.assertEqual(hook, expected_output) + + def test_finds_and_compiles_android_hooks_with_an_exception_handler(self): + hook = android_hook('filesystem/pwd') + + expected_output = """if (Java.available) { + + try { + + // From Frida documentation: + // "ensure that the current thread is attached to the VM and call fn" + // + // We also handle the exception that could happen within the callback as + // it does not seem to bubble outside of it. + Java.perform(function () { + + try { + + // Determines the current working directory, based +// on the applications filesDir + +var ActivityThread = Java.use('android.app.ActivityThread'); + +var currentApplication = ActivityThread.currentApplication(); +var context = currentApplication.getApplicationContext(); + +var response = { + status: 'success', + error_reason: NaN, + type: 'current-working-directory', + data: { + cwd: context.getFilesDir().getAbsolutePath().toString() + } +}; + +send(response); + +// -- Sample Java +// +// getApplicationContext().getFilesDir().getAbsolutePath() + + + } catch (err) { + + var response = { + status: 'error', + error_reason: err.message, + type: 'java-perform-exception', + data: {} + }; + + send(response); + } + }); + + } catch (err) { + + var response = { + status: 'error', + error_reason: err.message, + type: 'global-exception', + data: {} + }; + + send(response); + } + +} else { + + var response = { + status: 'error', + error_reason: 'Java runtime is not available.', + type: 'global-exception', + data: {} + }; + + send(response); +} +""" + self.assertEqual(hook, expected_output) + + def test_finds_and_compiles_android_hooks_without_an_exception_handler(self): + hook = android_hook('filesystem/pwd', skip_trycatch=True) + + expected_output = """// Determines the current working directory, based +// on the applications filesDir + +var ActivityThread = Java.use('android.app.ActivityThread'); + +var currentApplication = ActivityThread.currentApplication(); +var context = currentApplication.getApplicationContext(); + +var response = { + status: 'success', + error_reason: NaN, + type: 'current-working-directory', + data: { + cwd: context.getFilesDir().getAbsolutePath().toString() + } +}; + +send(response); + +// -- Sample Java +// +// getApplicationContext().getFilesDir().getAbsolutePath() +""" + + self.assertEqual(hook, expected_output) diff --git a/tests/utils/test_update_checker.py b/tests/utils/test_update_checker.py index 602ed1da..f1840a05 100644 --- a/tests/utils/test_update_checker.py +++ b/tests/utils/test_update_checker.py @@ -7,8 +7,8 @@ class TestUpdateChecker(unittest.TestCase): @mock.patch('objection.utils.update_checker.random') - def test_check_if_update_is_skipped_if_false_random(self, random): - random.choice.return_value = False + def test_check_if_update_is_skipped_if_false_random(self, mock_random): + mock_random.choice.return_value = False with capture(check_version) as o: output = o