android 进程注入 工具,Xposed注入实现分析及免重启定制

原标题:Xposed注入实现分析及免重启定制

Xposed的实现解析

Xposed简介

Xposed已经作为Android的Java层Hook大哥好多年了,不仅仅是安全研究者和逆向工程师手中的神器,还拥有着很多的插件开发者,在官方仓库收录的插件也已经超过了1000+个,而其服务的用户以各搞机论坛用户为主,也是不尽其数。

Xposed的代码是完全开源的,并且代码量也不算很大,对于安全研究者来说呢,我觉得研究一下Xposed的原理还是很有必要的。

Xposed的github地址:

https://github.com/rovo89/Xposed

https://github.com/rovo89/XposedBridge

本文使用Android4.4的源码进行分析。

如何实现全局注入

简单的讲,Xposed是通过替换修改过的app_process来注入Zygote以实现的全局注入。

Android的启动过程

想要知道Xposed是如何实现的全局注入,首先要分析一下Android系统的启动过程,就可以很方便的理解Xposed选择的注入点。

我画了张图,Android系统的启动大致如下:

8c8afb327969ea9a0f5308554491b281.png

从图中可以很方便的理解从BootLoader到显示系统界面的过程。

应用孵化器 - Zygote

Zygote介绍

Zygote,中文是"受精卵"...实际上他就是一个孵化器,所有应用程序的进程都是由它fork出来的。

USER PID PPID VSIZE RSS WCHAN PC NAME

root 155150974041876ffffffff b75761e0 S zygote

drm 1561185084064ffffffff b7599ff6 S / system/bin/drmserver

media 15716929615656ffffffff b755bff6 S / system/bin/mediaserver

install 158165801228c04d1048 b7507bb6 S / system/bin/installd

keystore 1591101722088c03f4a25 b7561ff6 S / system/bin/keystore

root 160110964c044881c 080ddd03 S / system/xbin/su

system1871810403860ffffffff b75b0ff6 S / system/bin/surfaceflinger

root 343200c01cfbd5 00000000S flush- 8: 16

root 4146267321412c02f4452 b74c6bb6 S / system/bin/ sh

root 4186266881464c01c0a90 b752e1e0 S logcat

system42415562927646384ffffffff b7575ff6 S system_server

u0_a44 50615556425275032ffffffff b75779eb S com.android.systemui

u0_a0 55115552548425696ffffffff b75779eb S android.process.acore

wifi 5651107762912c01c0a90 b741d1e0 S / system/bin/wpa_supplicant

system56615552646821732ffffffff b75779eb S com.android.settings

u0_a22 60615552127624932ffffffff b75779eb S com.android.inputmethod.latin

radio 62315553935628428ffffffff b75779eb S com.android.phone

u0_a23 63715555768045252ffffffff b75779eb S com.android.launcher

u0_a29 67315552030020324ffffffff b75779eb S com.android.music

u0_a16 69315552153225876ffffffff b75779eb S android.process.media

u0_a49 71915551758018600ffffffff b75779eb S com.android.smspush

u0_a15 80615552163219920ffffffff b75779eb S com.android.dialer

bluetooth 82115552282822284ffffffff b75779eb S com.android.bluetooth

dhcp 845166561452c01c0a90 b74e6ab6 S / system/bin/dhcpcd

u0_a6 91615552782822036ffffffff b75779eb S com.android.calendar

u0_a7 94315551963623176ffffffff b75779eb S com.android.providers.calendar

u0_a12 96715552115621960ffffffff b75779eb S com.android.deskclock

u0_a17 101915552830024912ffffffff b75779eb S com.android.email

u0_a18 107815552497620436ffffffff b75779eb S com.android.exchange

u0_a28 116815553085223100ffffffff b75779eb S com.android.mms

u0_a32 121515551757619140ffffffff b75779eb S com.android.onetimeinitializer

u0_a47 125115551773219072ffffffff b75779eb S com.android.voicedialer

可以看到,这些包名格式的进程的PPID(父进程PID)都是155,即zygote进程。

Zygote启动之后会建立一个Socket Server,然后fork自身成为一个新的进程——system_server,并让它成为"前端代言人",当system_server接收到打开进程的请求时,它就会通过Socket跟Zygote通讯,Zygote就再fork自身称为新的进程。

Zygote的启动

在init.rc中可以看到,Zygote是通过app_process启动的。

service zygote / system/bin/app_process -Xzygote / system/bin --zygote --start- system-server

这时app_process的任务也很简单,就是把自己的进程名变成Zygote,然后反射调用Zygote的主方法,即com.android.internal.os.ZygoteInit的main()。之后Zygote做的事情就是在介绍中说过的了。

Hook Zygote

那么Xposed就是通过修改app_process的方式来实现注入Zygote的。

项目地址:

https://github.com/rovo89/Xposed即是修改app_process的源码,在app_main.cpp的main函数中可以看到。

isXposedLoaded = xposed::initialize(zygote, startSystemServer, className, argc, argv);

if(zygote) {

runtime.start(isXposedLoaded ? XPOSED_CLASS_DOTS_ZYGOTE : "com.android.internal.os.ZygoteInit",

startSystemServer ? "start-system-server": "");

} elseif(className) {

//Remainder of args get passed to startup classmain()

runtime.mClassName = className;

runtime.mArgC = argc - i;

runtime.mArgV = argv + i;

runtime.start(isXposedLoaded ? XPOSED_CLASS_DOTS_TOOLS : "com.android.internal.os.RuntimeInit",

application ? "application": "tool");

}

可以看到,Xposed首先尝试加载自己的库,如果成功的话,就将runtime.start的类名替换成自己的类:de.robv.android.xposed.XposedBridge,因为app_processs是采用反射main()的方法来调用加载方法,所以只要在这个类中定义一个main()方法,就可以为所欲为了,最后只需在末尾调用一下com.android.internal.os.ZygoteInit的main()让系统继续跑下去就可以实现对Zygote的Hook。

注入应用进程

既然已经拿到了Zygote的所有权了,但是现在Application还没跑起来呢,如果这个时候就执行Hook模块,肯定是无法Hook到相关代码的。那么这时候就有必要提一下从Zygote接受到请求之后做了什么,我又简单的画了张图:

f8feadc737da05384550c9cfaf584a12.png

值得注意的是,因为fork之后的子进程是会继承父进程的状态继续往下跑,所以到handleChildProc的时候,此时已经是子进程在执行程序了,而ActivityThread也是在新进程中loop。

在Activity的loop中,可以接收的消息也包括了Appcalition的生命周期BIND_APPLICATION和EXIT_APPLICATION。处理BIND_APPLICATION的时候是直接调用了handleBindApplication方法,Xposed便是在Zygote中Hook了这个方法,在这里面执行用户的Hook模块,因为在这个时候,已经可以拿到应用的classLoader了,然后将此classLoader封装成一个LoadPackageParam再传给各个模块的handleLoadPackage即可。

在XposedBridge的main中可看到其调用了initForZygote():

protectedstaticvoidmain(String[] args){

...

if(isZygote) {

XposedInit.hookResources();

XposedInit.initForZygote();

}

...

}

initForZygote中又Hook了handleBindApplication:

staticvoidinitForZygote() throws Throwable{

...

findAndHookMethod(ActivityThread.class, "handleBindApplication", "android.app.ActivityThread.AppBindData", newXC_MethodHook() {

@ Override

protectedvoidbeforeHookedMethod(MethodHookParam param) throws Throwable {

ActivityThread activityThread = (ActivityThread) param.thisObject;

ApplicationInfo appInfo = (ApplicationInfo) getObjectField(param.args[ 0], "appInfo");

String reportedPackageName = appInfo.packageName. equals( "android") ? "system": appInfo.packageName;

SELinuxHelper.initForProcess(reportedPackageName);

ComponentName instrumentationName = (ComponentName) getObjectField(param.args[ 0], "instrumentationName");

if(instrumentationName != null) {

Log.w(TAG, "Instrumentation detected, disabling framework for "+ reportedPackageName);

XposedBridge.disableHooks = true;

return;

}

CompatibilityInfo compatInfo = (CompatibilityInfo) getObjectField(param.args[ 0], "compatInfo");

if(appInfo.sourceDir == null)

return;

setObjectField(activityThread, "mBoundApplication", param.args[ 0]);

loadedPackagesInProcess. add(reportedPackageName);

LoadedApk loadedApk = activityThread.getPackageInfoNoCheck(appInfo, compatInfo);

XResources.setPackageNameForResDir(appInfo.packageName, loadedApk.getResDir());

XC_LoadPackage.LoadPackageParam lpparam = newXC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks);

lpparam.packageName = reportedPackageName;

lpparam.processName = (String) getObjectField(param.args[ 0], "processName");

lpparam.classLoader = loadedApk.getClassLoader();

lpparam.appInfo = appInfo;

lpparam.isFirstApplication = true;

XC_LoadPackage.callAll(lpparam);

if(reportedPackageName. equals(INSTALLER_PACKAGE_NAME))

hookXposedInstaller(lpparam.classLoader);

}

});

...

}

Xposed在Zygote进程中执行完这些之后,每次fork出来的都是handleBindApplication已经被Hook过的进程,所以当每个应用进程被打开的时候,都会最开始就先执行Xposed的模块插件,然后才开始执行本身的代码。这样就实现了注入到每个应用程序来进行Hook。

如何实现Hook

findAndHookMethod

findAndHookMethod想必是大家写Xposed插件最常用及最熟悉的一个方法,我们就通过这个方法来分析Xposed是如何实现的Java Hook。

publicstaticXC_MethodHook.Unhook findAndHookMethod( Class> clazz, StringmethodName, Object... parameterTypesAndCallback){

if(parameterTypesAndCallback.length == 0|| !(parameterTypesAndCallback[parameterTypesAndCallback.length -1] instanceofXC_MethodHook))

thrownewIllegalArgumentException( "no callback defined");

XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length -1];

Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));

returnXposedBridge.hookMethod(m, callback);

}

大家知道这个方法的参数是个可变长参数列表,然后它会取最后一个参数来作为callback,即hook时的注入的代码。然后通过反射的方法来得到原本的Method。最后将这两个传给hookMethod。

publicstaticXC_MethodHook. Unhook hookMethod(Member hookMethod, XC_MethodHook callback){

...

if(newMethod) {

Class> declaringClass = hookMethod.getDeclaringClass();

intslot;

Class>[] parameterTypes;

Class> returnType;

if(runtime == RUNTIME_ART) {

slot = 0;

parameterTypes = null;

returnType = null;

} elseif(hookMethod instanceofMethod) {

slot = getIntField(hookMethod, "slot");

parameterTypes = ((Method) hookMethod).getParameterTypes();

returnType = ((Method) hookMethod).getReturnType();

} else{

slot = getIntField(hookMethod, "slot");

parameterTypes = ((Constructor>) hookMethod).getParameterTypes();

returnType = null;

}

AdditionalHookInfo additionalInfo = newAdditionalHookInfo(callbacks, parameterTypes, returnType);

hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);

}

...

}

可以看到hookMethod实际上时调用了一个native函数hookMethodNative,其他参数都很好理解,但是取的这个slot是个什么东西?翻了一下百度,在Dalvik的源码中dalvik/vm/reflect.c:

staticint methodToSlot( constMethod* meth)

{

ClassObject* clazz = meth->clazz;

int slot;

if(dvmIsDirectMethod(meth)) {

slot = meth - clazz->directMethods;

slot = -(slot+ 1);

} else{

slot = meth - clazz->virtualMethods;

}

returnslot;

}

原来这个slot就是此method在ClassObject的methods中的偏移,directMethods为负,virtualMethods为正。

偷梁换柱

hookMethodNative对应C函数XposedBridge_hookMethodNative

void XposedBridge_hookMethodNative(JNIEnv* env, jclass clazz, jobject reflectedMethodIndirect,

jobject declaredClassIndirect, jint slot, jobject additionalInfoIndirect) {

// Usage errors?

if(declaredClassIndirect == NULL|| reflectedMethodIndirect == NULL) {

dvmThrowIllegalArgumentException( "method and declaredClass must not be null");

return;

}

// Find the internal representation of the method

ClassObject* declaredClass = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), declaredClassIndirect);

Method* method = dvmSlotToMethod(declaredClass, slot);

if(method == NULL) {

dvmThrowNoSuchMethodError( "Could not get internal representation for method");

return;

}

if(isMethodHooked(method)) {

// already hooked

return;

}

// Save a copy of the original method and other hook info

XposedHookInfo* hookInfo = (XposedHookInfo*) calloc( 1, sizeof(XposedHookInfo));

memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));

hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));

hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));

// Replace method with our own code

SET_METHOD_FLAG(method, ACC_NATIVE);

method->nativeFunc = &hookedMethodCallback;

method->insns = ( constu2*) hookInfo;

method->registersSize = method->insSize;

method->outsSize = 0;

if(PTR_gDvmJit != NULL) {

// reset JIT cache

char currentValue = *((char*)PTR_gDvmJit + MEMBER_OFFSET_VAR(DvmJitGlobals,codeCacheFull));

if(currentValue == 0|| currentValue == 1) {

MEMBER_VAL(PTR_gDvmJit, DvmJitGlobals, codeCacheFull) = true;

} else{

ALOGE( "Unexpected current value for codeCacheFull: %d", currentValue);

}

}

}

首先是通过slot获取到Method对象在内存中的Method结构体指针,然后将它设置为一个native方法,再将他的nativeFunc即执行时对应的函数地址设置为hookedMethodCallback的指针,将保存着原method信息的hookInfo暂时存放在本是dalvik字节码的insns,reflectedMethod是原method的java对象,additionalInfo是AdditionalHookInfo对象。

然后当调用到这个方法的时候,执行的就变成了hookedMethodCallback函数了。

voidhookedMethodCallback( constu4* args, JValue* pResult, constMethod* method, ::Thread* self) {

...

dvmCallMethod( self, (Method*) methodXposedBridgeHandleHookedMethod, NULL, &result,

originalReflected, ( int) original, additionalInfo, thisObject, argsArray);

...

}

主要就是又调了一个java方法methodXposedBridgeHandleHookedMethod,解释一下各个参数,originalReflected就是上面hookInfo的reflectedMethod,original是原method结构体指针,后面的不必解释了吧。

privatestaticObjecthandleHookedMethod(Member method, int originalMethodId, ObjectadditionalInfoObj,

ObjectthisObject, Object[] args) throws Throwable {

...

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{

ObjectlastResult = 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);

...

}

那么methodXposedBridgeHandleHookedMethod做的事情也很好理解,就是先把所有的beforeHookedMethod给调用一遍,然后再还原原来的method调用一遍,最后再把所有的afterHookMethod的给调用一遍,就完成了。

还原执行

执行原来的方法调用的是native方法invokeOriginalMethodNative。

void XposedBridge_invokeOriginalMethodNative( constu4* args, JValue* pResult,

constMethod* method, ::Thread* self) {

Method* meth = (Method*) args[ 1];

if(meth == NULL) {

meth = dvmGetMethodFromReflectObj((Object*) args[ 0]);

if(isMethodHooked(meth)) {

meth = (Method*) meth->insns;

}

}

ArrayObject* params = (ArrayObject*) args[ 2];

ClassObject* returnType = (ClassObject*) args[ 3];

Object* thisObject = (Object*) args[ 4]; // null for static methods

ArrayObject* argList = (ArrayObject*) args[ 5];

// invoke the method

pResult->l = dvmInvokeMethod(thisObject, meth, argList, params, returnType, true);

return;

}

emm..就是通过拿到备份的method结构体,然后直接调用dvmInvokeMethod即可。

改造更新免重启的Xposed

大家在写Xposed模块的时候是不是有一个很不爽的地方,就是每次改两行代码之后又得重启,非常的烦,那么清楚了注入的原理之后,发现这点好像不是必须的!只是rovo89可能是考虑到性能的问题,所以才采用这种方案。实际上,只要轻轻的改动两行代码,就可以实现免重启更新!

Xposed原加载机制

Xposed原本是在app_process的时候一次性将模块列表读取,Load之后将其hook类放到一个Set里面,在de.robv.android.xposed.XposedBridge的main()中可以看到:

protectedstaticvoidmain(String[] args){

...

if(isZygote) {

XposedInit.hookResources();

XposedInit.initForZygote();

}

XposedInit.loadModules();

...

}

待到应用启动的时候再分别调用每个handleLoadPackage,之后就不再读取插件。因为这样,所以每次fork之后,插件已经被加载在内存中了,就算更新插件也不会被读取加载,所以必须得重启才能使得插件生效。

处理办法

之前说到,Xposed通过HookhandleBindApplication来进入每个应用进程,那只要在这个时候再loadModules,那不就OK了吗?

在loadModule中,进行加载不仅有handleLoadPackage,还有hook资源及hook命令行程序的包。

privatestaticvoidloadModule( Stringapk, ClassLoader topClassLoader) {

...

if(moduleInstance instanceofIXposedHookZygoteInit) {

IXposedHookZygoteInit.StartupParam param = newIXposedHookZygoteInit.StartupParam();

param.modulePath = apk;

param.startsSystemServer = startsSystemServer;

((IXposedHookZygoteInit) moduleInstance).initZygote(param);

}

if(moduleInstance instanceofIXposedHookLoadPackage)

XposedBridge.hookLoadPackage( newIXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));

if(moduleInstance instanceofIXposedHookInitPackageResources)

XposedBridge.hookInitPackageResources( newIXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));

} else{

if(moduleInstance instanceofIXposedHookCmdInit) {

IXposedHookCmdInit.StartupParam param = newIXposedHookCmdInit.StartupParam();

param.modulePath = apk;

param.startClassName = startClassName;

((IXposedHookCmdInit) moduleInstance).initCmdApp(param);

...

}

但是我的需求就是只需要处理handleLoadPackage就可以了,所以给这个参数加个开关isLoadHookLoadPackage,我的代码如下:

if(XposedBridge.isZygote) {

if(moduleInstance instanceofIXposedHookZygoteInit) {

IXposedHookZygoteInit.StartupParam param = newIXposedHookZygoteInit.StartupParam();

param.modulePath = apk;

param.startsSystemServer = startsSystemServer;

((IXposedHookZygoteInit) moduleInstance).initZygote(param);

}

if(moduleInstance instanceofIXposedHookLoadPackage)

if(isLoadHookLoadPackage)

{

XposedBridge.hookLoadPackage( newIXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));

}

if(moduleInstance instanceofIXposedHookInitPackageResources)

XposedBridge.hookInitPackageResources( newIXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));

} else{

if(moduleInstance instanceofIXposedHookCmdInit) {

IXposedHookCmdInit.StartupParam param = newIXposedHookCmdInit.StartupParam();

param.modulePath = apk;

param.startClassName = startClassName;

((IXposedHookCmdInit) moduleInstance).initCmdApp(param);

}

}

实际上就是简单的加了一个if而已,然后在main的地方改成:

XposedInit.loadModules( false);

最后就是在hookhandleBindApplication的地方加上XposedInit.loadModules(true);

findAndHookMethod(ActivityThread.class, "handleBindApplication", "android.app.ActivityThread.AppBindData", newXC_MethodHook() {

@Override

protectedvoidbeforeHookedMethod(MethodHookParam param)throwsThrowable{

XposedInit.loadModules( true);

...

嗯,炒鸡简单,改源码不超过2分钟。

编译

改源码两分钟,编译五小时。

rovo89还提供了另外一个项目:

https://github.com/rovo89/XposedTools

因为Xposed编译需要依赖AOSP来进行编译,这个工具就是专门用来编译Xposed源码的,使用方法也很简单,填好build.conf然后一把梭就可以了,具体的可以查看百度。

需要注意的是,上叙的源码是新版的xposed,就是支持5.0+art的,而我使用的是4.4.4的源码,模拟器也是4.4,所以clone源码的时候要-b master来切换分支。然后修改的代码也大同小异,就是loadmodule多了个startClassName参数,可以通过getStartClassName()方法获取,就是这样:

XposedBridge.loadModules(getStartClassName(),true);

因为这个老工程是个Eclipse工程,XposedTool是依赖于Gradle,所以无法使用XposedTool编译。

直接使用Android Studio - Build Apk最后把文件名改成XposedBridge.jar即可,最后你可以选择直接修改Xposed Installer将其中的XposedBridge.jar替换再安装,或者在手机上安装好Xposed,然后将修改好的XposedBridge.jar替换/data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar,重启,就可以愉快的调插件玩耍了。

- End -

e9fc403857a92f46f9ce883bb357c03e.png

看雪ID:葫芦娃

https://bbs.pediy.com/user-715510.htm

本文由看雪论坛葫芦娃原创

转载请注明来自看雪社区

热门图书推荐:

戳立即购买!

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值