From 28963b08849244b29ec1235ca2a620a373d65515 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 Dec 2023 20:13:54 +0000 Subject: [PATCH] Add JS bindings for fast interceptor --- bindings/gumjs/gumquickinterceptor.c | 71 +++++++++ bindings/gumjs/gumv8interceptor.cpp | 65 +++++++++ bindings/gumjs/runtime/core.js | 7 + tests/gumjs/script.c | 206 +++++++++++++++++++++++++++ 4 files changed, 349 insertions(+) diff --git a/bindings/gumjs/gumquickinterceptor.c b/bindings/gumjs/gumquickinterceptor.c index b05c6807b4..c86a935552 100644 --- a/bindings/gumjs/gumquickinterceptor.c +++ b/bindings/gumjs/gumquickinterceptor.c @@ -164,6 +164,7 @@ static void gum_quick_interceptor_detach (GumQuickInterceptor * self, GumQuickInvocationListener * listener); GUMJS_DECLARE_FUNCTION (gumjs_interceptor_detach_all) GUMJS_DECLARE_FUNCTION (gumjs_interceptor_replace) +GUMJS_DECLARE_FUNCTION (gumjs_interceptor_replace_fast) static void gum_quick_replace_entry_free (GumQuickReplaceEntry * entry); static void gum_quick_replace_entry_revert_and_free ( GumQuickReplaceEntry * entry); @@ -284,6 +285,7 @@ static const JSCFunctionListEntry gumjs_interceptor_entries[] = JS_CFUNC_DEF ("_attach", 3, gumjs_interceptor_attach), JS_CFUNC_DEF ("detachAll", 0, gumjs_interceptor_detach_all), JS_CFUNC_DEF ("_replace", 0, gumjs_interceptor_replace), + JS_CFUNC_DEF ("_replaceFast", 0, gumjs_interceptor_replace_fast), JS_CFUNC_DEF ("revert", 0, gumjs_interceptor_revert), JS_CFUNC_DEF ("flush", 0, gumjs_interceptor_flush), }; @@ -719,6 +721,75 @@ gum_quick_replace_entry_free (GumQuickReplaceEntry * entry) g_slice_free (GumQuickReplaceEntry, entry); } +GUMJS_DEFINE_FUNCTION (gumjs_interceptor_replace_fast) +{ + GumQuickInterceptor * self; + gpointer target, replacement_function, original_function; + JSValue replacement_value; + GumQuickReplaceEntry * entry = NULL; + GumReplaceReturn replace_ret; + GumQuickNativeCallback * c; + + self = gumjs_get_parent_module (core); + + if (!_gum_quick_args_parse (args, "pO", &target, &replacement_value)) + goto propagate_exception; + + if (!_gum_quick_native_pointer_get (ctx, replacement_value, core, + &replacement_function)) + goto propagate_exception; + + entry = g_slice_new (GumQuickReplaceEntry); + entry->interceptor = self->interceptor; + entry->target = target; + entry->replacement = JS_DupValue (ctx, replacement_value); + entry->ctx = ctx; + + replace_ret = gum_interceptor_replace_fast (self->interceptor, target, + replacement_function, &original_function); + if (replace_ret != GUM_REPLACE_OK) + goto unable_to_replace; + + c = JS_GetOpaque (entry->replacement, core->native_callback_class); + if (c != NULL) + c->interceptor_replacement_count++; + + g_hash_table_insert (self->replacement_by_address, target, entry); + + return _gum_quick_native_pointer_new (ctx, + GSIZE_TO_POINTER (original_function), core); + +unable_to_replace: + { + switch (replace_ret) + { + case GUM_REPLACE_WRONG_SIGNATURE: + _gum_quick_throw (ctx, "unable to intercept function at %p; " + "please file a bug", target); + break; + case GUM_REPLACE_ALREADY_REPLACED: + _gum_quick_throw_literal (ctx, "already replaced this function"); + break; + case GUM_REPLACE_POLICY_VIOLATION: + _gum_quick_throw_literal (ctx, "not permitted by code-signing policy"); + break; + case GUM_REPLACE_WRONG_TYPE: + _gum_quick_throw_literal (ctx, "wrong type"); + break; + default: + g_assert_not_reached (); + } + + goto propagate_exception; + } +propagate_exception: + { + gum_quick_replace_entry_free (entry); + + return JS_EXCEPTION; + } +} + static void gum_quick_replace_entry_revert_and_free (GumQuickReplaceEntry * entry) { diff --git a/bindings/gumjs/gumv8interceptor.cpp b/bindings/gumjs/gumv8interceptor.cpp index 242fbbf0f3..c0bb0db8ab 100644 --- a/bindings/gumjs/gumv8interceptor.cpp +++ b/bindings/gumjs/gumv8interceptor.cpp @@ -145,6 +145,7 @@ static void gum_v8_invocation_listener_destroy ( GumV8InvocationListener * listener); GUMJS_DECLARE_FUNCTION (gumjs_interceptor_detach_all) GUMJS_DECLARE_FUNCTION (gumjs_interceptor_replace) +GUMJS_DECLARE_FUNCTION (gumjs_interceptor_replace_fast) static void gum_v8_replace_entry_free (GumV8ReplaceEntry * entry); GUMJS_DECLARE_FUNCTION (gumjs_interceptor_revert) GUMJS_DECLARE_FUNCTION (gumjs_interceptor_flush) @@ -267,6 +268,7 @@ static const GumV8Function gumjs_interceptor_functions[] = { "_attach", gumjs_interceptor_attach }, { "detachAll", gumjs_interceptor_detach_all }, { "_replace", gumjs_interceptor_replace }, + { "_replaceFast", gumjs_interceptor_replace_fast }, { "revert", gumjs_interceptor_revert }, { "flush", gumjs_interceptor_flush }, @@ -751,6 +753,69 @@ GUMJS_DEFINE_FUNCTION (gumjs_interceptor_replace) } } +GUMJS_DEFINE_FUNCTION (gumjs_interceptor_replace_fast) +{ + gpointer target, replacement_function, original_function; + if (!_gum_v8_args_parse (args, "pp", &target, &replacement_function)) + return; + auto replacement_function_value = info[1]; + + auto entry = g_slice_new (GumV8ReplaceEntry); + entry->interceptor = module->interceptor; + entry->target = target; + entry->replacement = new Global (isolate, replacement_function_value); + + auto replace_ret = gum_interceptor_replace_fast (module->interceptor, target, + replacement_function, &original_function); + + if (replace_ret == GUM_REPLACE_OK) + { + auto native_callback = Local::New (isolate, + *core->native_callback); + auto instance = replacement_function_value.As (); + if (native_callback->HasInstance (instance)) + { + auto callback = (GumV8NativeCallback *) + instance->GetInternalField (1).As ()->Value (); + callback->interceptor_replacement_count++; + } + + g_hash_table_insert (module->replacement_by_address, target, entry); + + info.GetReturnValue ().Set ( _gum_v8_native_pointer_new ( + GSIZE_TO_POINTER (original_function), core)); + } + else + { + delete entry->replacement; + g_slice_free (GumV8ReplaceEntry, entry); + } + + switch (replace_ret) + { + case GUM_REPLACE_OK: + break; + case GUM_REPLACE_WRONG_SIGNATURE: + { + _gum_v8_throw_ascii (isolate, "unable to intercept function at %p; " + "please file a bug", target); + break; + } + case GUM_REPLACE_ALREADY_REPLACED: + _gum_v8_throw_ascii_literal (isolate, "already replaced this function"); + break; + case GUM_REPLACE_POLICY_VIOLATION: + _gum_v8_throw_ascii_literal (isolate, + "not permitted by code-signing policy"); + break; + case GUM_REPLACE_WRONG_TYPE: + _gum_v8_throw_ascii_literal (isolate, "wrong type"); + break; + default: + g_assert_not_reached (); + } +} + static void gum_v8_replace_entry_free (GumV8ReplaceEntry * entry) { diff --git a/bindings/gumjs/runtime/core.js b/bindings/gumjs/runtime/core.js index 2441acbe4c..24140835c4 100644 --- a/bindings/gumjs/runtime/core.js +++ b/bindings/gumjs/runtime/core.js @@ -487,6 +487,13 @@ if (globalThis.Interceptor !== undefined) { Interceptor._replace(target, replacement, data); } }, + replaceFast: { + enumerable: true, + value: function (target, replacement) { + Memory._checkCodePointer(target); + return Interceptor._replaceFast(target, replacement); + } + }, }); } diff --git a/tests/gumjs/script.c b/tests/gumjs/script.c index 6873963ca3..fe934985cb 100644 --- a/tests/gumjs/script.c +++ b/tests/gumjs/script.c @@ -83,6 +83,16 @@ TESTLIST_BEGIN (script) TESTENTRY (interceptor_should_support_native_pointer_values) TESTENTRY (interceptor_should_handle_bad_pointers) TESTENTRY (interceptor_should_refuse_to_attach_without_any_callbacks) + TESTGROUP_BEGIN ("Interceptor/Fast") + TESTENTRY (function_can_be_replaced_fast) + TESTENTRY (function_can_be_replaced_fast_and_called_immediately) + TESTENTRY (function_can_be_reverted_fast) + TESTENTRY (interceptor_should_support_native_pointer_values_fast) + TESTENTRY (interceptor_should_handle_bad_pointers_fast) + TESTENTRY (function_can_be_replaced_and_call_original_fast) + TESTENTRY (function_can_be_replaced_fast_performance) + TESTENTRY (function_can_be_replaced_and_call_original_fast_performance) + TESTGROUP_END () #ifdef HAVE_DARWIN TESTENTRY (interceptor_and_js_should_not_deadlock) #endif @@ -487,6 +497,10 @@ typedef struct _GumInvokeTargetContext GumInvokeTargetContext; typedef struct _GumCrashExceptorContext GumCrashExceptorContext; typedef struct _TestTrigger TestTrigger; +typedef int (* TargetFunctionInt) (int arg); + +static TargetFunctionInt target_function_original = NULL; + struct _GumInvokeTargetContext { GumScript * script; @@ -577,6 +591,8 @@ static void on_incoming_debug_message (GumInspectorServer * server, static void on_outgoing_debug_message (const gchar * message, gpointer user_data); +static int target_function_int_replacement (int arg); + #ifdef HAVE_DARWIN static gpointer interceptor_attacher_worker (gpointer data); static void empty_invocation_callback (GumInvocationContext * context, @@ -7157,6 +7173,196 @@ TESTCASE (interceptor_should_refuse_to_attach_without_any_callbacks) "Error: expected at least one callback"); } +TESTCASE (function_can_be_replaced_fast) +{ + COMPILE_AND_LOAD_SCRIPT ( + "Interceptor.replaceFast(" GUM_PTR_CONST "," + " new NativeCallback(arg => {" + " send(arg);" + " return 1337;" + "}, 'int', ['int']));", + target_function_int); + + EXPECT_NO_MESSAGES (); + g_assert_cmpint (target_function_int (7), ==, 1337); + EXPECT_SEND_MESSAGE_WITH ("7"); + EXPECT_NO_MESSAGES (); + + gum_script_unload_sync (fixture->script, NULL); + target_function_int (1); + EXPECT_NO_MESSAGES (); +} + +TESTCASE (function_can_be_replaced_fast_and_called_immediately) +{ + COMPILE_AND_LOAD_SCRIPT ( + "const address = " GUM_PTR_CONST ";" + "Interceptor.replaceFast(address," + " new NativeCallback(arg => {" + " send(arg);" + " return 1337;" + "}, 'int', ['int']));" + "const f = new NativeFunction(address, 'int', ['int']," + " { scheduling: 'exclusive' });" + "f(7);" + "Interceptor.flush();" + "f(8);", + target_function_int); + EXPECT_SEND_MESSAGE_WITH ("8"); + EXPECT_NO_MESSAGES (); +} + +TESTCASE (function_can_be_reverted_fast) +{ + COMPILE_AND_LOAD_SCRIPT ( + "Interceptor.replaceFast(" GUM_PTR_CONST ", new NativeCallback(arg => {" + " send(arg);" + " return 1337;" + "}, 'int', ['int']));" + "Interceptor.revert(" GUM_PTR_CONST ");", + target_function_int, target_function_int); + + EXPECT_NO_MESSAGES (); + target_function_int (7); + EXPECT_NO_MESSAGES (); +} + +TESTCASE (interceptor_should_support_native_pointer_values_fast) +{ + COMPILE_AND_LOAD_SCRIPT ( + "const value = { handle: " GUM_PTR_CONST " };" + "Interceptor.replaceFast(value," + " new NativeCallback(arg => 1337, 'int', ['int']));", + target_function_int); + EXPECT_NO_MESSAGES (); + g_assert_cmpint (target_function_int (7), ==, 1337); + EXPECT_NO_MESSAGES (); +} + +TESTCASE (interceptor_should_handle_bad_pointers_fast) +{ + if (!check_exception_handling_testable ()) + return; + + COMPILE_AND_LOAD_SCRIPT ( + "Interceptor.replaceFast(ptr(0x42)," + " new NativeCallback(() => {}, 'void', []));"); + EXPECT_ERROR_MESSAGE_WITH (ANY_LINE_NUMBER, + "Error: access violation accessing 0x42"); +} + +TESTCASE (function_can_be_replaced_and_call_original_fast) +{ + int ret = target_function_int (1); + + COMPILE_AND_LOAD_SCRIPT ( + "let func;" + "let addr = Interceptor.replaceFast(" GUM_PTR_CONST "," + " new NativeCallback(arg => {" + " return func(arg) + 1;" + " }, 'int', ['int']));" + "func = new NativeFunction(addr, 'int', ['int']);", + target_function_int); + + EXPECT_NO_MESSAGES (); + g_assert_cmpint (target_function_int (1), ==, ret + 1); + EXPECT_NO_MESSAGES (); +} + +TESTCASE (function_can_be_replaced_fast_performance) +{ + GTimer * timer; + gdouble duration_default, duration_fast; + + target_function_original = NULL; + + timer = g_timer_new (); + + COMPILE_AND_LOAD_SCRIPT ( + "Interceptor.replace(" GUM_PTR_CONST "," GUM_PTR_CONST ");", + target_function_int, target_function_int_replacement); + + g_timer_reset (timer); + for (gsize i = 0; i != 1000000; i++) + { + g_assert_cmpint (target_function_int (7), ==, 1337); + } + duration_default = g_timer_elapsed (timer, NULL); + + gum_script_unload_sync (fixture->script, NULL); + + COMPILE_AND_LOAD_SCRIPT ( + "Interceptor.replaceFast(" GUM_PTR_CONST "," GUM_PTR_CONST ");", + target_function_int, target_function_int_replacement); + + g_timer_reset (timer); + for (gsize i = 0; i != 1000000; i++) + { + g_assert_cmpint (target_function_int (7), ==, 1337); + } + duration_fast = g_timer_elapsed (timer, NULL); + + g_timer_destroy (timer); + + g_print (" ", + duration_fast, duration_default, duration_fast / duration_default); +} + +TESTCASE (function_can_be_replaced_and_call_original_fast_performance) +{ + GTimer * timer; + gdouble duration_default, duration_fast; + + target_function_original = NULL; + + timer = g_timer_new (); + + COMPILE_AND_LOAD_SCRIPT ( + "let orig_ptr = ptr(" GUM_PTR_CONST ");" + "let orig = ptr(" GUM_PTR_CONST ");" + "Interceptor.replace(orig," GUM_PTR_CONST ");" + "orig_ptr.writePointer(orig);", + &target_function_original, target_function_int, + target_function_int_replacement); + + g_timer_reset (timer); + for (gsize i = 0; i != 1000000; i++) + { + g_assert_cmpint (target_function_int (7), ==, 1652); + } + duration_default = g_timer_elapsed (timer, NULL); + + gum_script_unload_sync (fixture->script, NULL); + + COMPILE_AND_LOAD_SCRIPT ( + "let orig_ptr = ptr(" GUM_PTR_CONST ");" + "let orig = Interceptor.replaceFast(" GUM_PTR_CONST "," GUM_PTR_CONST ");" + "orig_ptr.writePointer(orig);", + &target_function_original, target_function_int, + target_function_int_replacement); + + g_timer_reset (timer); + for (gsize i = 0; i != 1000000; i++) + { + g_assert_cmpint (target_function_int (7), ==, 1652); + } + duration_fast = g_timer_elapsed (timer, NULL); + + g_timer_destroy (timer); + + g_print (" ", + duration_fast, duration_default, duration_fast / duration_default); +} + +GUM_NOINLINE static int +target_function_int_replacement (int arg) +{ + if (target_function_original == NULL) + return 1337; + else + return 1337 + target_function_original (arg); +} + #ifdef HAVE_DARWIN TESTCASE (interceptor_and_js_should_not_deadlock)