原创

Xposed 源码剖析(一)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/a314131070/article/details/81092526

0x00 简介

  • 是什么:
      Xposed framework是一个基于Android系统实现的能够给用户提供修改系统层面或第三方APP功能的框架服务。
  • 如何工作:
      Android中有一个叫做Zygote的核心进程,它会随Android系统的启动而启动,然后加载系统所需的类,最后再调用初始化方法。每一个APP的进程都是从Zygote进程fork出的子进程,这个进程的文件是/system/bin/app_process
      当安装Xposed framework后,Xposed framework会替换一个新的app_process/system/bin/中,同时还会替换虚拟机和其他若干文件。Zygote启动时,会加载Xposed所需的JAR包(/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar)至系统目录,并启动Xposed替换的虚拟机。
      Xposed framework的主要接口由XposedBridge.jar提供,框架的核心功能在替换的虚拟机中实现。

0x01 示例代码

用户可以使用Xposed frameworkHook方法,下面是作者本人给出的一个示例:

package de.robv.android.xposed.mods.tutorial;

import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import android.graphics.Color;
import android.widget.TextView;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        if (!lpparam.packageName.equals("com.android.systemui"))
            return;

        findAndHookMethod("com.android.systemui.statusbar.policy.Clock", lpparam.classLoader, "updateClock", new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                TextView tv = (TextView) param.thisObject;
                String text = tv.getText().toString();
                tv.setText(text + " :)");
                tv.setTextColor(Color.RED);
            }
        });
    }
}

这个示例代码实现了对Andoird系统时钟输出样子和颜色的修改,完整的源码地址是https://github.com/rovo89/XposedExamples/tree/master/RedClock。如此简单的几行代码,它的内部实现机制是怎么的呢?想知道这些,最好的办法当然是分析源码。

0x02 Hook流程分析

这里使用Xposed framework Hook java.net.URLEncoder类的encode方法作为例子分析,调用逻辑如下所示:

public class Tutorial implements IXposedHookLoadPackage {
    public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
        findAndHookMethod("java.net.URLEncoder", lpparam.classLoader, "encode", String.class, String.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
            }
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
            }
        });

handleLoadPackage会在任意一个APP加载的时候被调用,然后就可以在这个接口内部根据包名做逻辑判断,如果是Hook方法所在的包,则进一步调用findAndHookMethodHook指定的方法。
通过findAndHookMethod()接口找到指定方法并进行Hook。它的参数较多,第一个参数是类名,第二个参数是类加载器,第三个参数是方法名,后面是可变参数,把方法的参数类型依次作为参数,最后一个参数是XC_MethodHook()对象。另外,在其内部重写beforeHookedMethodafterHookedMethod方法,这两个方法分别会在Hook方法执行的前后执行,用户可以在其中实现自己的逻辑。
findAndHookMethod这个接口的实现在XposedHelpers.java这个文件中,代码如下:

public static XC_MethodHook.Unhook findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback) {
    return findAndHookMethod(findClass(className, classLoader), methodName, parameterTypesAndCallback);
}
    
public static XC_MethodHook.Unhook findAndHookMethod(Class<?> clazz, String methodName, Object... parameterTypesAndCallback) {
    if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook))
        throw new IllegalArgumentException("no callback defined");

    XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1];
    Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));

    return XposedBridge.hookMethod(m, callback);
}

首先,接口内部会根据类名和类加载器查找Class对象,再根据Class对象和方法名去查找Method对象,最后调用XposedBridge.hookMethod完成对方法的Hook
看一下方法查找的实现:

public static Method findMethodExact(String className, ClassLoader classLoader, String methodName, Object... parameterTypes) {
        return findMethodExact(findClass(className, classLoader), methodName, getParameterClasses(classLoader, parameterTypes));
    }
    
public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) {
        String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";

        if (methodCache.containsKey(fullMethodName)) {
            Method method = methodCache.get(fullMethodName);
            if (method == null)
                throw new NoSuchMethodError(fullMethodName);
            return method;
        }

        try {
            Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
            method.setAccessible(true);
            methodCache.put(fullMethodName, method);
            return method;
        } catch (NoSuchMethodException e) {
            methodCache.put(fullMethodName, null);
            throw new NoSuchMethodError(fullMethodName);
        }
    }

findMethodExact内部,fullMethodName变量就是这个方法的完全表示名,形如java.net.URLEncoder#encode(java.lang.String,java.lang.String)#exact。使用方法的完全表示名在methodCache表中查询,如果这个Method对象在表中就从表中获取并返回,如果不在就创建一个新的Method“`对象放入表中并返回。

进一步看一下XposedBridge.hookMethod方法的实现,代码片段如下:

public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
    ...
    CopyOnWriteSortedSet<XC_MethodHook> callbacks;
    ...
    callbacks.add(callback);
    
    if (newMethod) {
        Class<?> declaringClass = hookMethod.getDeclaringClass();
        ...
        AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType);
        hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);
    }

    return callback.new Unhook(hookMethod);
}

private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);

这里的逻辑比较清晰,如果这个方法不存在对应的回调对象集合,那么就创建一个并把回调对象放入这个集合中。然后如果是新Hook的方法,获取这个方法的参数和返回类型等信息作为参数去调用hookMethodNative方法,这是一个native方法,它注册的地方在libXposed_common.cpp中,注册逻辑如下:

#define NATIVE_METHOD(className, functionName, signature) \
  { #functionName, signature, reinterpret_cast<void*>(className ## _ ## functionName) }
  
int register_natives_XposedBridge(JNIEnv* env, jclass clazz) {
    const JNINativeMethod methods[] = {
        ...
        NATIVE_METHOD(XposedBridge, hookMethodNative, "(Ljava/lang/reflect/Member;Ljava/lang/Class;ILjava/lang/Object;)V"),
        ...
    };
    return env->RegisterNatives(clazz, methods, NELEM(methods));
}

NATIVE_METHOD是一个宏定义,根据这个宏定义,进一步找到hookMethodNative对应的native方法是XposedBridge_hookMethodNative。这里以art为例,这个方法的实现在libXposed_art.cpp文件中,代码如下:

void XposedBridge_hookMethodNative(JNIEnv* env, jclass, jobject javaReflectedMethod,
            jobject, jint, jobject javaAdditionalInfo) {
    ...

    // 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);
}

通过FromReflectedMethod获取要Hook方法对应的ArtMethod对象,然后再调用EnableXposedHook接口。在art虚拟机中,每一个加载的类方法都有一个对应的ArtMethod对象,它的实现在ArtMethod.cc中,下面是EnableXposedHook方法的代码实现:

void ArtMethod::EnableXposedHook(ScopedObjectAccess& soa, jobject additional_info) {
  ...

  // 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);
}

这部分代码是关键所在,首先创建一个备份的ArtMethod,并添加访问标志位kAccXposedOriginalMethod,表示其为Hook方法的原方法,然后为备份的ArtMethod创建对应的Method对象。把Method对象、方法额外信息和原始方法保存至XposedHookInfo结构体中,并调用SetEntryPointFromJni()把这个结构体变量的内存地址保存在ArtMethod对象中。这个位置原本是用来保存native方法的入口地址的,既然使用了这个位置,那么就必须把对应的标志位清除,代码实现的最后调用SetAccessFlags((GetAccessFlags() & ~kAccNative & ~kAccSynchronized) | kAccXposedHookedMethod)来完成标志位的清除。这并不会有问题,此时这个ArtMethod对象对应是Hook后的方法,这个方法的实现不是native的。
接着看下面的代码逻辑,需要结合StackReplaceMethod()的实现来分析:

static void StackReplaceMethod(Thread* thread, void* arg) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
  struct StackReplaceMethodVisitor FINAL : public StackVisitor {
    StackReplaceMethodVisitor(Thread* thread_in, ArtMethod* search, ArtMethod* replace)
        : StackVisitor(thread_in, nullptr, StackVisitor::StackWalkKind::kSkipInlinedFrames),
          search_(search), replace_(replace) {};

    bool VisitFrame() SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
      if (GetMethod() == search_) {
        SetMethod(replace_);
      }
      return true;
    }

    ArtMethod* search_;
    ArtMethod* replace_;
  };

  ArtMethod* search = reinterpret_cast<ArtMethod*>(arg);

  // We cannot use GetXposedOriginalMethod() because the access flags aren't modified yet.
  auto hook_info = reinterpret_cast<const XposedHookInfo*>(search->GetEntryPointFromJni());
  ArtMethod* replace = hook_info->originalMethod;

  StackReplaceMethodVisitor visitor(thread, search, replace);
  visitor.WalkStack();
}

它实现的功能是挂起所有线程并在每一个线程中查找是否存在这个修改后的ArtMethod对象,有就替换成未修改前的ArtMethod对象。

再回到EnableXposedHook中,接着调用 SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler())和SetEntryPointFromInterpreter(artInterpreterToCompiledCodeBridge)分别设置机器指令和解释器的入口地址。从解释器入口进入之后会调用artInterpreterToCompiledCodeBridge,下面是artInterpreterToCompiledCodeBridge代码片段:

 extern "C" void artInterpreterToCompiledCodeBridge(Thread* self, const DexFile::CodeItem* code_item,
                                                   ShadowFrame* shadow_frame, JValue* result) {
  ...
  method->Invoke(self, shadow_frame->GetVRegArgs(arg_offset),
                 (shadow_frame->NumberOfVRegs() - arg_offset) * sizeof(uint32_t),
                 result, method->GetInterfaceMethodIfProxy(sizeof(void*))->GetShorty());
}

前面的代码逻辑都先不用关注,看最后一行调用了ArtMehodInvoke方法,这正是机器指令执行的路径。所以说,当被Hook的方法被调用时,不管是机器指令还是解释器执行的,都会进入ArtMethod::Invoke()中,下面是它的代码片段:

void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result, const char* shorty) {
    ...
    if (LIKELY(have_quick_code)) {
      if (!IsStatic()) {
        (*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
      } else {
        (*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
      }
    }
    ...
}

ArtMethod::Invoke()会进一步调用art_quick_invoke_stubart_quick_invoke_stub的内部实现是汇编语言的,art_quick_invoke_stub与具体的机器架构相关,这里以arm架构为例:

ENTRY art_quick_invoke_stub
    push   {r0, r4, r5, r9, r11, lr}       @ spill regs
    .save  {r0, r4, r5, r9, r11, lr}
    .pad #24
    .cfi_adjust_cfa_offset 24
    .cfi_rel_offset r0, 0
    .cfi_rel_offset r4, 4
    .cfi_rel_offset r5, 8
    .cfi_rel_offset r9, 12
    .cfi_rel_offset r11, 16
    .cfi_rel_offset lr, 20
    mov    r11, sp                         @ save the stack pointer
    .cfi_def_cfa_register r11
    mov    r9, r3                          @ move managed thread pointer into r9
#ifdef ARM_R4_SUSPEND_FLAG
    mov    r4, #SUSPEND_CHECK_INTERVAL     @ reset r4 to suspend check interval
#endif
    add    r5, r2, #4                      @ create space for method pointer in frame

    sub    r5, sp, r5                      @ reserve & align *stack* to 16 bytes: native calling
    and    r5, #0xFFFFFFF0                 @ convention only aligns to 8B, so we have to ensure ART
    mov    sp, r5                          @ 16B alignment ourselves.

    add    r0, sp, #4                      @ pass stack pointer + method ptr as dest for memcpy
    bl     memcpy                          @ memcpy (dest, src, bytes)
    ldr    r0, [r11]                       @ restore method*
    ldr    r1, [sp, #4]                    @ copy arg value for r1
    ldr    r2, [sp, #8]                    @ copy arg value for r2
    ldr    r3, [sp, #12]                   @ copy arg value for r3
    mov    ip, #0                          @ set ip to 0
    str    ip, [sp]                        @ store NULL for method* at bottom of frame
    ldr    ip, [r0, #METHOD_QUICK_CODE_OFFSET_32]  @ get pointer to the code
    blx    ip                              @ call the method
    mov    sp, r11                         @ restore the stack pointer
    ldr    ip, [sp, #24]                   @ load the result pointer
    strd   r0, [ip]                        @ store r0/r1 into result pointer
    pop    {r0, r4, r5, r9, r11, lr}       @ restore spill regs
    .cfi_restore r0
    .cfi_restore r4
    .cfi_restore r5
    .cfi_restore r9
    .cfi_restore lr
    .cfi_adjust_cfa_offset -24
    bx     lr
END art_quick_invoke_stub

汇编指令blx ip跳转到entry_point_from_compiled_code_指定的地址,这个地址就是通过SetEntryPointFromQuickCompiledCode()函数设置的,可知此时执行流跳入到GetQuickProxyInvokeHandler()的地址中,即artQuickProxyInvokeHandler:

runtime_asm_entrypoints.h

extern "C" void art_quick_proxy_invoke_handler();
static inline const void* GetQuickProxyInvokeHandler() {
   return reinterpret_cast<const void*>(art_quick_proxy_invoke_handler);
}

quick_trampoline_entrypoint.cc

extern "C" uint64_t artQuickProxyInvokeHandler(
    ArtMethod* proxy_method, mirror::Object* receiver, Thread* self, ArtMethod** sp) {
        ...
          if (is_xposed) {
            jmethodID proxy_methodid = soa.EncodeMethod(proxy_method);
            self->EndAssertNoThreadSuspension(old_cause);
            JValue result = InvokeXposedHandleHookedMethod(soa, shorty, rcvr_jobj, proxy_methodid, args);
            local_ref_visitor.FixupReferences();
            return result.GetJ();
          }
        ...
    }

这个代理方法会判断当前是否是xposed环境,如果是就调用InvokeXposedHandleHookedMethod()方法。

JValue InvokeXposedHandleHookedMethod(ScopedObjectAccessAlreadyRunnable& soa, const char* shorty, jobject rcvr_jobj, jmethodID method, std::vector<jvalue>& args) {
      ...
      const XposedHookInfo* hookInfo = soa.DecodeMethod(method)->GetXposedHookInfo();
        // Call XposedBridge.handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
  //                                      Object thisObject, Object[] args)
  jvalue invocation_args[5];
  invocation_args[0].l = hookInfo->reflectedMethod;
  invocation_args[1].i = 1;
  invocation_args[2].l = hookInfo->additionalInfo;
  invocation_args[3].l = rcvr_jobj;
  invocation_args[4].l = args_jobj;
  jobject result =
      soa.Env()->CallStaticObjectMethodA(ArtMethod::xposed_callback_class,
                                         ArtMethod::xposed_callback_method,
                                         invocation_args);
        ...
}

这个方法的内部实现是,先获取保存在ArtMethod对象中的XposedHookInfo数据,然后通过CallStaticObjectMethodA接口调用XposedBridge.handleHookedMethod()方法,注意此时已经从native中跳入到JAVA里了。继续看XposedBridge.handleHookedMethod()方法的实现:

    private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
            Object thisObject, Object[] args) throws Throwable {
        ...
                MethodHookParam param = new MethodHookParam();
        param.method = method;
        param.thisObject = thisObject;
        param.args = args;

        // call "before method" callbacks
        int beforeIdx = 0;
        do {
            try {
                ((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
            } catch (Throwable t) {
                XposedBridge.log(t);

                // reset result (ignoring what the unexpectedly exiting callback did)
                param.setResult(null);
                param.returnEarly = false;
                continue;
            }

            if (param.returnEarly) {
                // skip remaining "before" callbacks and corresponding "after" callbacks
                beforeIdx++;
                break;
            }
        } while (++beforeIdx < callbacksLength);

        // call original method if not requested otherwise
        if (!param.returnEarly) {
            try {
                param.setResult(invokeOriginalMethodNative(method, originalMethodId,
                        additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));
            } catch (InvocationTargetException e) {
                param.setThrowable(e.getCause());
            }
        }

        // call "after method" callbacks
        int afterIdx = beforeIdx - 1;
        do {
            Object lastResult =  param.getResult();
            Throwable lastThrowable = param.getThrowable();

            try {
                ((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
            } catch (Throwable t) {
                XposedBridge.log(t);

                // reset to last result (ignoring what the unexpectedly exiting callback did)
                if (lastThrowable == null)
                    param.setResult(lastResult);
                else
                    param.setThrowable(lastThrowable);
            }
        } while (--afterIdx >= 0);

        // return
        if (param.hasThrowable())
            throw param.getThrowable();
        else
            return param.getResult();
                
    }

进入handleHookedMethod()后,首先调用所有before method的回调方法,然后调用原始方法,最后调用所有after method的回调方法。到这里就可以看出跟之前应用层的代码形成的对应关系。

Xposed framework通过invokeOriginalMethodNative方法去调用原方法,其对应的native方法如下:

jobject XposedBridge_invokeOriginalMethodNative(JNIEnv* env, jclass, jobject javaMethod,
            jint isResolved, jobjectArray, jclass, jobject javaReceiver, jobjectArray javaArgs) {
    ScopedFastNativeObjectAccess soa(env);
    if (UNLIKELY(!isResolved)) {
        ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaMethod);
        if (LIKELY(artMethod->IsXposedHookedMethod())) {
            javaMethod = artMethod->GetXposedHookInfo()->reflectedMethod;
        }
    }
#if PLATFORM_SDK_VERSION >= 23
    return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
#else
    return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs, true);
#endif
}

InvokeMethod()的实现在Reflection.cc中,代码如下所示:

jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
  ...
  mirror::ArtMethod* m = mirror::ArtMethod::FromReflectedMethod(soa, javaMethod);
  
  ...
  // Invoke the method.
  JValue result;
  uint32_t shorty_len = 0;
  const char* shorty = m->GetShorty(&shorty_len);
  ArgArray arg_array(shorty, shorty_len);
  StackHandleScope<1> hs(soa.Self());
  MethodHelper mh(hs.NewHandle(m));
  if (!arg_array.BuildArgArrayFromObjectArray(soa, receiver, objects, mh)) {
    CHECK(soa.Self()->IsExceptionPending());
    return nullptr;
  }

  InvokeWithArgArray(soa, m, &arg_array, &result, shorty);

通过FromReflectedMethod()获取到Method对象对应的ArtMethod对象,这个就是在Hook时创建的原始ArtMethod对象的备份。然后,再通过InvokeWithArgArray调用原始的方法。

0x03 总结

至此,完成了对Xposed framework Hook的源码分析。我们再整体看一下它的流程,当Hook一个方法时,首先需要提供方法的所在的包名、方法名、参数类型以及回调对象去调用Hook接口;Xposedart虚拟机中找到方法对应的ArtMethod对象,备份这个ArtMethod对象,然后修改这个对象的解释器和机器指令入口,关键所在。然后把每个线程中加载的这个被修改后的ArtMethod对象替换成原始备份的ArtMethod对象。最后在修改的机器指令入口地址指向的函数里,实现回调方法和原始方法的调用。

很多细节还没有涉及,下面会从/system/bin/app_process的实现着手分析其框架部分代码的实现。

文章最后发布于: 2018-07-18 09:36:36
展开阅读全文
0 个人打赏

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 1024 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览