Understanding the Xposed Framework for ART Runtime
I answered a questions in Zhihu about "What's the implementation of Xposed Framework on ART Runtime". I realize that this is an interesting question because I did a research on hooking/interception years ago. Therefore, I then went through the code and here is a breakdown notes on the Xposed Framework.
First, the Xposed Framework contains many sub-repositories. I will explain three core components in detail. They are Xposed, XposedInstaller, android_art.
Essentially, the Xposed framework use hooking techniques to replace method pointer inside runtime. The basic steps of hooking in the Xposed framework is following:
- XposedInstaller replace
/system/bin/app_process
with customizedapp_process
. For ART runtime XposedInstaller will also replace customize ART runtime. - XposedInstaller invoke
xposed::initialize()
in customizedapp_process
to start hooking - customize ART runtime (
Xposed/android_art
) is patched with aEnableXposedHook()
method EnableXposedHook()
will switch method pointer by swapping ArtMethod pointer in ClassLinker in ART Runtime
XposedInstaller Repository
install()
method in InstallerFragment.java
class will do the main task. Let's go through some codes.
The following code snippet will backup original app_process
, copy customized app_process
and set proper file owner and user mod.
private boolean install() {
// deleted lines ...
if (installMode == INSTALL_MODE_NORMAL) {
// Normal installation
messages.add(getString(R.string.file_mounting_writable, "/system"));
if (mRootUtil.executeWithBusybox("mount -o remount,rw /system", messages) != 0) {
messages.add(getString(R.string.file_mount_writable_failed, "/system"));
messages.add(getString(R.string.file_trying_to_continue));
}
if (new File("/system/bin/app_process.orig").exists()) {
messages.add(getString(R.string.file_backup_already_exists, "/system/bin/app_process.orig"));
} else {
if (mRootUtil.executeWithBusybox("cp -a /system/bin/app_process /system/bin/app_process.orig", messages) != 0) {
messages.add("");
messages.add(getString(R.string.file_backup_failed, "/system/bin/app_process"));
return false;
} else {
messages.add(getString(R.string.file_backup_successful, "/system/bin/app_process.orig"));
}
mRootUtil.executeWithBusybox("sync", messages);
}
messages.add(getString(R.string.file_copying, "app_process"));
if (mRootUtil.executeWithBusybox("cp -a " + appProcessFile.getAbsolutePath() + " /system/bin/app_process", messages) != 0) {
messages.add("");
messages.add(getString(R.string.file_copy_failed, "app_process", "/system/bin"));
return false;
}
if (mRootUtil.executeWithBusybox("chmod 755 /system/bin/app_process", messages) != 0) {
messages.add("");
messages.add(getString(R.string.file_set_perms_failed, "/system/bin/app_process"));
return false;
}
if (mRootUtil.executeWithBusybox("chown root:shell /system/bin/app_process", messages) != 0) {
messages.add("");
messages.add(getString(R.string.file_set_owner_failed, "/system/bin/app_process"));
return false;
}
}
// deleted lines ...
}
Xposed Repository
For ART runtime Xposed repository provides a libxposed_art.cpp
containing
XposedBridge_hookMethodNative()
method. This method will invoke artMethod->EnableXposedHook
method in customized ART Runtime.
Let's see the code.
void XposedBridge_hookMethodNative(JNIEnv* env, jclass, jobject javaReflectedMethod,
jobject, jint, jobject javaAdditionalInfo) {
// Detect usage errors.
if (javaReflectedMethod == nullptr) {
#if PLATFORM_SDK_VERSION >= 23
ThrowIllegalArgumentException("method must not be null");
#else
ThrowIllegalArgumentException(nullptr, "method must not be null");
#endif
return;
}
// Get the ArtMethod of the method to be hooked.
ScopedObjectAccess soa(env);
ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaReflectedMethod);
// Hook the method
artMethod->EnableXposedHook(soa, javaAdditionalInfo);
}
android_art
Repository
Let's read the code of EnableXposedHook method.
void ArtMethod::EnableXposedHook(ScopedObjectAccess& soa, jobject additional_info) {
if (UNLIKELY(IsXposedHookedMethod())) {
// Already hooked
return;
} else if (UNLIKELY(IsXposedOriginalMethod())) {
// This should never happen
ThrowIllegalArgumentException(StringPrintf("Cannot hook the method backup: %s", PrettyMethod(this).c_str()).c_str());
return;
}
// Create a backup of the ArtMethod object
auto* cl = Runtime::Current()->GetClassLinker();
ArtMethod* backup_method = cl->AllocArtMethodArray(soa.Self(), 1);
backup_method->CopyFrom(this, cl->GetImagePointerSize());
backup_method->SetAccessFlags(backup_method->GetAccessFlags() | kAccXposedOriginalMethod);
// Create a Method/Constructor object for the backup ArtMethod object
mirror::AbstractMethod* reflect_method;
if (IsConstructor()) {
reflect_method = mirror::Constructor::CreateFromArtMethod(soa.Self(), backup_method);
} else {
reflect_method = mirror::Method::CreateFromArtMethod(soa.Self(), backup_method);
}
reflect_method->SetAccessible<false>(true);
// Save extra information in a separate structure, stored instead of the native method
XposedHookInfo* hookInfo = reinterpret_cast<XposedHookInfo*>(calloc(1, sizeof(XposedHookInfo)));
hookInfo->reflectedMethod = soa.Vm()->AddGlobalRef(soa.Self(), reflect_method);
hookInfo->additionalInfo = soa.Env()->NewGlobalRef(additional_info);
hookInfo->originalMethod = backup_method;
SetEntryPointFromJni(reinterpret_cast<uint8_t*>(hookInfo));
ThreadList* tl = Runtime::Current()->GetThreadList();
soa.Self()->TransitionFromRunnableToSuspended(kSuspended);
tl->SuspendAll("Hooking method");
{
MutexLock mu(soa.Self(), *Locks::thread_list_lock_);
tl->ForEach(StackReplaceMethod, this);
}
tl->ResumeAll();
soa.Self()->TransitionFromSuspendedToRunnable();
SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler());
SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge);
// Adjust access flags
SetAccessFlags((GetAccessFlags() & ~kAccNative & ~kAccSynchronized) | kAccXposedHookedMethod);
}
The hooking includes these steps:
- backup original ArtMethod pointer
- store the hooking method pointer, additional information and backup method pointer to hookInfo structure
- replace the entrypoint of original method by invoking
SetEntryPointFromJni()
method
In the end, the original method will be replace with new one.
In summary, the Xposed framework will replace the ArtMethod pointer with the new code. Note that the hooking can only be done in framework layer above ART runtime. This means that any native method written by C/C++ (NDK) still cannot be hooked. Furthermore, some functionalities in Android are only implemented in native code. Therefore, there are still several limitations for the Xposed framework. But, I guess it's enough for some people to create interesting modules. Thanks for reading. Happy hacking!