Skip to content

Commit

Permalink
feat: allocateInstance & invokeConstructor
Browse files Browse the repository at this point in the history
  • Loading branch information
rushiiMachine committed Aug 28, 2024
1 parent cfbe892 commit 67834a9
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 1 deletion.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ from original Xposed and it should work with almost no modificiations.

Additionally, XposedBridge contains these new methods:

- `allocateInstance` - Allocates a class instance without calling any constructors
Can be used as a simpler alternative to `sun.misc.Unsafe#allocateInstance`
- `invokeConstructor` - Invokes a constructor for an existing instance, most useful in conjunction
raw instance allocation.
- `makeClassInheritable` - Makes a final class inheritable, see LSPlant doc for more info
- `deoptimizeMethod` - Deoptimises method to solve inline issues, see LSPlant doc for more info
- `disableProfileSaver` - Disables Android Profile Saver to try to prevent ahead of time compilation
Expand Down
23 changes: 23 additions & 0 deletions core/src/androidTest/java/com/aliucord/hook/Dummy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.aliucord.hook;

public class Dummy {
public boolean initialized;

Object a;
int b;
Integer c;
String d;

public Dummy() {
initialized = true;
}

public Dummy(Object a, int b, Integer c, String d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}

public Dummy(Object... varargs) {}
}
63 changes: 63 additions & 0 deletions core/src/androidTest/java/com/aliucord/hook/UnitTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import android.os.Build;
Expand Down Expand Up @@ -116,6 +117,68 @@ public void shouldNotHookField() throws Throwable {
XposedBridge.hookMethod(UnitTest.class.getDeclaredField("counter"), XC_MethodReplacement.DO_NOTHING);
}

@Test
public void shouldAllocateInstance() {
var instance = XposedBridge.allocateInstance(Dummy.class);
assertFalse(instance.initialized);
}

@Test
public void shouldInvokeConstructor() throws Throwable {
var instance = XposedBridge.allocateInstance(Dummy.class);
assertNotNull("failed to alloc", instance);
assertFalse("constructor not supposed to be called", instance.initialized);

var success = XposedBridge.invokeConstructor(instance, Dummy.class.getDeclaredConstructor());
assertTrue("invokeConstructor failed", success);
assertTrue("constructor not called", instance.initialized);
}

@Test(expected = IllegalArgumentException.class)
public void shouldNotInvokeVarargsConstructor() throws Throwable {
XposedBridge.invokeConstructor(new Dummy(), Dummy.class.getDeclaredConstructor(Object[].class));
}

@Test(expected = IllegalArgumentException.class)
public void shouldFailInvokeConstructorWrongArgCount() throws Throwable {
XposedBridge.invokeConstructor(
new Dummy(),
Dummy.class.getDeclaredConstructor(),
"balls"
);
}

@Test(expected = IllegalArgumentException.class)
public void shouldFailInvokeConstructorWrongArgs() throws Throwable {
var constructor = Dummy.class.getDeclaredConstructor(Object.class, int.class, Integer.class, String.class);
XposedBridge.invokeConstructor(
new Dummy(),
constructor,
"balls",
Integer.valueOf(1),
"ballsv2",
new Object()
);
}

@Test
public void shouldInvokeArgsConstructor() throws Throwable {
var a = new Object();
var b = 42;
var c = Integer.valueOf(420);
var d = "balls";

var instance = XposedBridge.allocateInstance(Dummy.class);
var constructor = Dummy.class.getDeclaredConstructor(Object.class, int.class, Integer.class, String.class);
var success = XposedBridge.invokeConstructor(instance, constructor, a, b, c, d);
assertTrue("invokeConstructor failed", success);
assertFalse("wrong constructor called", instance.initialized);
assertEquals("a does not match", a, instance.a);
assertEquals("b does not match", b, instance.b);
assertEquals("c does not match", c, instance.c);
assertEquals("d does not match", d, instance.d);
}

@Test
public void shouldDisableProfileSaver() {
assertTrue(XposedBridge.disableProfileSaver());
Expand Down
13 changes: 12 additions & 1 deletion core/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,18 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include_directories(xz/linux/lib/xz xz/linux/include/linux xz/userspace)

add_library(aliuhook SHARED aliuhook.cpp elf_img.cpp profile_saver.cpp hidden_api.cpp xz/linux/lib/xz/xz_crc32.c xz/linux/lib/xz/xz_crc64.c xz/linux/lib/xz/xz_dec_bcj.c xz/linux/lib/xz/xz_dec_lzma2.c xz/linux/lib/xz/xz_dec_stream.c)
add_library(aliuhook SHARED
aliuhook.cpp
elf_img.cpp
profile_saver.cpp
hidden_api.cpp
invoke_constructor.cpp
xz/linux/lib/xz/xz_crc32.c
xz/linux/lib/xz/xz_crc64.c
xz/linux/lib/xz/xz_dec_bcj.c
xz/linux/lib/xz/xz_dec_lzma2.c
xz/linux/lib/xz/xz_dec_stream.c
)

find_package(lsplant REQUIRED CONFIG)
find_package(dobby REQUIRED CONFIG)
Expand Down
35 changes: 35 additions & 0 deletions core/src/main/cpp/aliuhook.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <cstdlib>
#include <cerrno>
#include "aliuhook.h"
#include "invoke_constructor.h"

int AliuHook::android_version = -1;
pine::ElfImg AliuHook::elf_img; // NOLINT(cert-err58-cpp)
Expand Down Expand Up @@ -110,6 +111,26 @@ Java_de_robv_android_xposed_XposedBridge_disableHiddenApiRestrictions(JNIEnv *en
return disable_hidden_api(env);
}

extern "C"
JNIEXPORT jobject JNICALL
Java_de_robv_android_xposed_XposedBridge_allocateInstance0(JNIEnv *env, jclass, jclass clazz) {
return env->AllocObject(clazz);
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_de_robv_android_xposed_XposedBridge_invokeConstructor0(JNIEnv *env, jclass, jobject instance, jobject constructor, jobjectArray args) {
jmethodID constructorMethodId = env->FromReflectedMethod(constructor);
if (!constructorMethodId) return JNI_FALSE;

if (!args) {
env->CallVoidMethod(instance, constructorMethodId);
return JNI_TRUE;
} else {
return InvokeConstructorWithArgs(env, instance, constructor, args);
}
}

JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *) {
JNIEnv *env;
Expand Down Expand Up @@ -154,5 +175,19 @@ JNI_OnLoad(JavaVM *vm, void *) {

LOGI("lsplant init finished");

res = LoadInvokeConstructorCache(env);
if (!res) {
LOGE("invoke_constructor init failed");
return JNI_ERR;
}

return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL
JNI_OnUnload(JavaVM *vm, void *) {
JNIEnv *env;
vm->GetEnv((void **) &env, JNI_VERSION_1_1);

UnloadInvokeConstructorCache(env);
}
210 changes: 210 additions & 0 deletions core/src/main/cpp/invoke_constructor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//
// Created by rushii on 2024-08-27.
//

#include "invoke_constructor.h"

// Based on https://github.com/toolfactory/narcissus/blob/c81c3d0a6f0fb5ee8ab444d170db21ff7fe8a7ad/src/main/c/narcissus.c

jclass Integer_class;
jclass int_class;
jmethodID Integer_intValue_methodID;

jclass Long_class;
jclass long_class;
jmethodID Long_longValue_methodID;

jclass Short_class;
jclass short_class;
jmethodID Short_shortValue_methodID;

jclass Character_class;
jclass char_class;
jmethodID Character_charValue_methodID;

jclass Boolean_class;
jclass boolean_class;
jmethodID Boolean_booleanValue_methodID;

jclass Byte_class;
jclass byte_class;
jmethodID Byte_byteValue_methodID;

jclass Float_class;
jclass float_class;
jmethodID Float_floatValue_methodID;

jclass Double_class;
jclass double_class;
jmethodID Double_doubleValue_methodID;

jmethodID Executable_getParameterTypes_methodID;

void throwIllegalArgumentException(JNIEnv* env, const char* message);
bool unboxArgs(JNIEnv *env, jobject method, jobjectArray args, jsize argsCount, jvalue* args_out);

bool LoadInvokeConstructorCache(JNIEnv *env) {
Integer_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Integer"));
if (env->ExceptionOccurred()) return false;
int_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Integer_class, env->GetStaticFieldID(Integer_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Integer_intValue_methodID = env->GetMethodID(Integer_class, "intValue", "()I");
if (env->ExceptionOccurred()) return false;

Long_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Long"));
if (env->ExceptionOccurred()) return false;
long_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Long_class, env->GetStaticFieldID(Long_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Long_longValue_methodID = env->GetMethodID(Long_class, "longValue", "()J");
if (env->ExceptionOccurred()) return false;

Short_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Short"));
if (env->ExceptionOccurred()) return false;
short_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Short_class, env->GetStaticFieldID(Short_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Short_shortValue_methodID = env->GetMethodID(Short_class, "shortValue", "()S");
if (env->ExceptionOccurred()) return false;

Character_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Character"));
if (env->ExceptionOccurred()) return false;
char_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Character_class, env->GetStaticFieldID(Character_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Character_charValue_methodID = env->GetMethodID(Character_class, "charValue", "()C");
if (env->ExceptionOccurred()) return false;

Boolean_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Boolean"));
if (env->ExceptionOccurred()) return false;
boolean_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Boolean_class, env->GetStaticFieldID(Boolean_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Boolean_booleanValue_methodID = env->GetMethodID(Boolean_class, "booleanValue", "()Z");
if (env->ExceptionOccurred()) return false;

Byte_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Byte"));
if (env->ExceptionOccurred()) return false;
byte_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Byte_class, env->GetStaticFieldID(Byte_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Byte_byteValue_methodID = env->GetMethodID(Byte_class, "byteValue", "()B");
if (env->ExceptionOccurred()) return false;

Float_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Float"));
if (env->ExceptionOccurred()) return false;
float_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Float_class, env->GetStaticFieldID(Float_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Float_floatValue_methodID = env->GetMethodID(Float_class, "floatValue", "()F");
if (env->ExceptionOccurred()) return false;

Double_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Double"));
if (env->ExceptionOccurred()) return false;
double_class = (jclass)env->NewGlobalRef(env->GetStaticObjectField(Double_class, env->GetStaticFieldID(Double_class, "TYPE", "Ljava/lang/Class;")));
if (env->ExceptionOccurred()) return false;
Double_doubleValue_methodID = env->GetMethodID(Double_class, "doubleValue", "()D");
if (env->ExceptionOccurred()) return false;

jclass Executable_class = env->FindClass("java/lang/reflect/Executable");
if (env->ExceptionOccurred()) return false;
Executable_getParameterTypes_methodID = env->GetMethodID(Executable_class, "getParameterTypes", "()[Ljava/lang/Class;");
if (env->ExceptionOccurred()) return false;

return true;
}

void UnloadInvokeConstructorCache(JNIEnv* env) {
env->DeleteGlobalRef(Integer_class);
env->DeleteGlobalRef(int_class);
env->DeleteGlobalRef(Long_class);
env->DeleteGlobalRef(long_class);
env->DeleteGlobalRef(Short_class);
env->DeleteGlobalRef(short_class);
env->DeleteGlobalRef(Character_class);
env->DeleteGlobalRef(char_class);
env->DeleteGlobalRef(Boolean_class);
env->DeleteGlobalRef(boolean_class);
env->DeleteGlobalRef(Byte_class);
env->DeleteGlobalRef(byte_class);
env->DeleteGlobalRef(Float_class);
env->DeleteGlobalRef(float_class);
env->DeleteGlobalRef(Double_class);
env->DeleteGlobalRef(double_class);
}

bool InvokeConstructorWithArgs(JNIEnv *env, jobject instance, jobject constructor, jobjectArray args) {
jmethodID constructorMethodId = env->FromReflectedMethod(constructor);
if (env->ExceptionOccurred()) return false;

jsize argsCount = env->GetArrayLength(args);
if (env->ExceptionOccurred()) return false;

auto* unboxedArgs = new jvalue[argsCount];
if (!unboxArgs(env, constructor, args, argsCount, unboxedArgs)) return false;

This comment has been minimized.

Copy link
@Vendicated

Vendicated Aug 28, 2024

Member

need to delete, no?


env->CallVoidMethodA(instance, constructorMethodId, unboxedArgs);

delete[] unboxedArgs;
return !env->ExceptionOccurred();
}

// Unbox a jobjectArray of method invocation args into a jvalue array.
bool unboxArgs(JNIEnv *env, jobject method, jobjectArray args, jsize argsCount, jvalue* args_out) {
// Get parameter types
auto parameterTypes = (jobjectArray)env->CallObjectMethod(method, Executable_getParameterTypes_methodID);
if (env->ExceptionOccurred()) return false;

jsize parameterCount = env->GetArrayLength(parameterTypes);
if (env->ExceptionOccurred()) return false;

if (argsCount != parameterCount) {
throwIllegalArgumentException(env, "Tried to invoke method with wrong number of arguments");
return false;
}

// Unbox non-varargs args
for (jsize i = 0; i < argsCount; i++) {
auto parameterType = (jclass)env->GetObjectArrayElement(parameterTypes, i);
if (env->ExceptionOccurred()) return false;

jobject arg = env->GetObjectArrayElement(args, i);
if (env->ExceptionOccurred()) return false;

jclass arg_type = arg == nullptr ? nullptr : env->GetObjectClass(arg);
if (env->ExceptionOccurred()) return false;

#define TRY_UNBOX_ARG(_prim_type, _Prim_type, _Boxed_type, _jvalue_field) \
if (env->IsSameObject(parameterType, _prim_type ## _class)) { \
if (arg == NULL) { \
throwIllegalArgumentException(env, "Tried to unbox a null argument; expected " #_Boxed_type); \
} else if (!env->IsSameObject(arg_type, _Boxed_type ## _class)) { \
throwIllegalArgumentException(env, "Tried to unbox arg of wrong type; expected " #_Boxed_type); \
} else { \
args_out[i]._jvalue_field = env->Call ## _Prim_type ## Method(arg, _Boxed_type ## _ ## _prim_type ## Value_methodID); \
} \
}

TRY_UNBOX_ARG(int, Int, Integer, i)
else TRY_UNBOX_ARG(long, Long, Long, j)
else TRY_UNBOX_ARG(short, Short, Short, s)
else TRY_UNBOX_ARG(char, Char, Character, c)
else TRY_UNBOX_ARG(boolean, Boolean, Boolean, z)
else TRY_UNBOX_ARG(byte, Byte, Byte, b)
else TRY_UNBOX_ARG(float, Float, Float, f)
else TRY_UNBOX_ARG(double, Double, Double, d)
else {
// Parameter type is not primitive -- check if arg is assignable from the parameter type
if (arg != nullptr && !env->IsAssignableFrom(arg_type, parameterType)) {
throwIllegalArgumentException(env, "Tried to invoke function with arg of incompatible type");
} else {
args_out[i].l = arg;
}
}

if (env->ExceptionOccurred()) return false;
}

return true;
}

void throwIllegalArgumentException(JNIEnv* env, const char* message) {
jclass cls = env->FindClass("java/lang/IllegalArgumentException");
if (cls) {
env->ThrowNew(cls, message);
}
}
Loading

0 comments on commit 67834a9

Please sign in to comment.