From d98323470bc715e1c9f2a26c38384d52550753e1 Mon Sep 17 00:00:00 2001 From: Megladon <30530996+IPMegladon@users.noreply.github.com> Date: Thu, 23 May 2024 23:15:14 +0200 Subject: [PATCH] (feat) Added memory replace command. (#656) * (feat) Added memory replace command. * refactor duplicate code. --- agent/src/generic/memory.ts | 7 ++++ agent/src/rpc/memory.ts | 2 ++ objection/commands/memory.py | 63 +++++++++++++++++++++++++++++++++++ objection/console/commands.py | 6 ++++ tests/commands/test_memory.py | 51 +++++++++++++++++++++++++++- 5 files changed, 128 insertions(+), 1 deletion(-) diff --git a/agent/src/generic/memory.ts b/agent/src/generic/memory.ts index 779cfbf7..820a29fd 100644 --- a/agent/src/generic/memory.ts +++ b/agent/src/generic/memory.ts @@ -45,6 +45,13 @@ export const search = (pattern: string, onlyOffsets: boolean = false): string[] return addresses.reduce((a, b) => a.concat(b)); }; +export const replace = (pattern: string, replace: number[]): string[] => { + return search(pattern, true).map((match) => { + write(match, replace); + return match; + }) +}; + export const write = (address: string, value: number[]): void => { new NativePointer(address).writeByteArray(value); }; diff --git a/agent/src/rpc/memory.ts b/agent/src/rpc/memory.ts index 8dc2b805..a925cb81 100644 --- a/agent/src/rpc/memory.ts +++ b/agent/src/rpc/memory.ts @@ -7,5 +7,7 @@ export const memory = { memoryListModules: (): Module[] => m.listModules(), memoryListRanges: (protection: string): RangeDetails[] => m.listRanges(protection), memorySearch: (pattern: string, onlyOffsets: boolean): string[] => m.search(pattern, onlyOffsets), + memoryReplace: (pattern: string, replace: number[]): string[] => m.replace(pattern, replace), memoryWrite: (address: string, value: number[]): void => m.write(address, value), + }; diff --git a/objection/commands/memory.py b/objection/commands/memory.py index 90bf3598..fb501c5d 100644 --- a/objection/commands/memory.py +++ b/objection/commands/memory.py @@ -36,6 +36,30 @@ def _should_only_dump_offsets(args: list) -> bool: return '--offsets-only' in args +def _is_string_pattern(args: list) -> bool: + """ + Checks if --string-pattern is in the list of tokens received form the + command line. + + :param args: + :return: + """ + + return len(args) > 0 and '--string-pattern' in args + + +def _is_string_replace(args: list) -> bool: + """ + Checks if --string-replace is in the list of tokens received form the + command line. + + :param args: + :return: + """ + + return len(args) > 0 and '--string-replace' in args + + def _should_output_json(args: list) -> bool: """ Checks if --json is in the list of tokens received from the command line. @@ -296,6 +320,45 @@ def find_pattern(args: list) -> None: click.secho('Unable to find the pattern in any memory region') +def replace_pattern(args: list) -> None: + """ + Searches the current processes accessible memory for a specific pattern and replaces it with given bytes or string. + + :param args: + :return: + """ + + if len(clean_argument_flags(args)) < 2: + click.secho('Usage: memory replace "" "" (--string-pattern) (--string-replace)', bold=True) + return + + # if we got a string as search pattern input, convert it to hex + if _is_string_pattern(args): + pattern = ' '.join(hex(ord(x))[2:] for x in args[0]) + else: + pattern = args[0] + + # if we got a string as replace input, convert it to int[], otherwise convert hex to int[] + replace = args[1] + if _is_string_replace(args): + replace = [ord(x) for x in replace] + else: + replace = [int(x, 16) for x in replace.split(' ')] + + click.secho('Searching for: {0}, replacing with: {1}'.format(pattern, args[1]), dim=True) + + api = state_connection.get_api() + data = api.memory_replace(pattern, replace) + + if len(data) > 0: + click.secho('Pattern replaced at {0} addresses'.format(len(data)), fg='green') + for address in data: + click.secho(address) + + else: + click.secho('Unable to find the pattern in any memory region') + + def write(args: list) -> None: """ Write an arbitrary amount of bytes to an arbitrary memory address. diff --git a/objection/console/commands.py b/objection/console/commands.py index 43130aa1..2702a1be 100644 --- a/objection/console/commands.py +++ b/objection/console/commands.py @@ -225,6 +225,12 @@ 'exec': memory.find_pattern }, + 'replace': { + 'meta': 'Search and replace pattern in the applications memory', + 'flags': ['--string-pattern', '--string-replace'], + 'exec': memory.replace_pattern + }, + 'write': { 'meta': 'Write raw bytes to a memory address. Use with caution!', 'flags': ['--string'], diff --git a/tests/commands/test_memory.py b/tests/commands/test_memory.py index ef966900..997faabc 100644 --- a/tests/commands/test_memory.py +++ b/tests/commands/test_memory.py @@ -2,7 +2,7 @@ from unittest import mock from objection.commands.memory import _is_string_input, dump_all, dump_from_base, list_modules, list_exports, \ - find_pattern + find_pattern, replace_pattern from ..helpers import capture @@ -207,6 +207,55 @@ def test_find_pattern_without_string_argument_with_offets_only(self, mock_api): expected_output = """Searching for: 41 41 41 Pattern matched at 1 addresses 0x08000000 +""" + + self.assertEqual(output, expected_output) + + + def test_replace_pattern_validates_arguments(self): + with capture(replace_pattern, []) as o: + output = o + + self.assertEqual(output, 'Usage: memory replace "" "" (--string-pattern) (--string-replace)\n') + + @mock.patch('objection.state.connection.state_connection.get_api') + def test_replace_pattern_without_string_argument(self, mock_api): + mock_api.return_value.memory_replace.return_value = ['0x08000000'] + + with capture(replace_pattern, ['41 41 41','41 42']) as o: + output = o + + expected_output = """Searching for: 41 41 41, replacing with: 41 42 +Pattern replaced at 1 addresses +0x08000000 +""" + + self.assertEqual(output, expected_output) + + @mock.patch('objection.state.connection.state_connection.get_api') + def test_replace_pattern_with_string_argument(self, mock_api): + mock_api.return_value.memory_replace.return_value = ['0x08000000'] + + with capture(replace_pattern, ['foo-bar-baz', '41 41', '--string-pattern']) as o: + output = o + + expected_output = """Searching for: 66 6f 6f 2d 62 61 72 2d 62 61 7a, replacing with: 41 41 +Pattern replaced at 1 addresses +0x08000000 +""" + + self.assertEqual(output, expected_output) + + @mock.patch('objection.state.connection.state_connection.get_api') + def test_replace_pattern_without_string_argument_with_offets_only(self, mock_api): + mock_api.return_value.memory_replace.return_value = ['0x08000000'] + + with capture(replace_pattern, ['41 41 41', 'ABC', '--string-replace']) as o: + output = o + + expected_output = """Searching for: 41 41 41, replacing with: ABC +Pattern replaced at 1 addresses +0x08000000 """ self.assertEqual(output, expected_output)