hook原理
了解Hook
我们知道,在Android操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步的向下执行。而“钩子”的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子勾上事件一样。并且能够在勾上事件时,处理一些自己特定的事件。如下图所示:
动态代理
传统的静态代理模式需要为每一个需要代理的类写一个代理类,如果需要代理的类有几百个那不是要累死?为了更优雅地实现代理模式,JDK提供了动态代理方式,可以简单理解为JVM可以在运行时帮我们动态生成一系列的代理类,这样我们就不需要手写每一个静态的代理类了。依然以购物为例,用动态代理实现如下:
public static void main(String[] args) {
Shopping people = new ShoppingImp();
System.out.println(Arrays.toString(people.doShopping(100)));
people = (Shopping) Proxy.newProxyInstance(Shopping.class.getClassLoader(),
people.getClass().getInterfaces(), new ShoppingHandler(people));
System.out.println(Arrays.toString(people.doShopping(100)));
}
Hook Android的startActivity方法
Android在启动的时候会创建ActivityThread
, 这是一个单例的对象,而startActivity
实际上是Instrumentation
中的execStartActivity()
来实现的。所有我们只要替换掉ActivityThread
中的Instrumentation
的对象成我们自己的方法。
创建代理类
public class ProxyInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;
public ProxyInstrumentation(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options){
// Hook之前, XXX到此一游!
Log.d(TAG, "sanfen到此一游!!!");
Log.d(TAG, "\n执行了startActivity, 参数如下: \n" + "who = [" + who + "], " +
"\ncontextThread = [" + contextThread + "], \ntoken = [" + token + "], " +
"\ntarget = [" + target + "], \nintent = [" + intent +
"], \nrequestCode = [" + requestCode + "], \noptions = [" + options + "]");
// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mBase, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
// 某该死的rom修改了 需要手动适配
throw new RuntimeException("do not support!!! pls adapt it");
}
}
}
通过反射修改ActivityThread
中的mInstrumentation
。
public static void hookStartActivity(){
try {
// 先获取到当前的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation mInstrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);
// 创建代理对象
Instrumentation evilInstrumentation = new ProxyInstrumentation(mInstrumentation);
// 偷梁换柱
mInstrumentationField.set(currentActivityThread, evilInstrumentation);
} catch (ClassNotFoundException
| NoSuchMethodException
| IllegalAccessException
| InvocationTargetException
| NoSuchFieldException e) {
e.printStackTrace();
}
}
执行效果,在运行startActivty的时候打出了一段日志。
AndFix使用
AndFix采用native hook的方式,这套方案直接使用dalvik_replaceMethod替换class中方法的实现。由于它并没有整体替换class, 而field在class中的相对地址在class加载时已确定,所以AndFix无法支持新增或者删除filed的情况(通过替换init与clinit只可以修改field的数值)。
也正因如此,Andfix可以支持的补丁场景相对有限,仅仅可以使用它来修复特定问题。结合之前的发布流程,我们更希望补丁对开发者是不感知的,即他不需要清楚这个修改是对补丁版本还是正式发布版本(事实上我们也是使用git分支管理+cherry-pick方式)。另一方面,使用native替换将会面临比较复杂的兼容性问题。
引入andfix
在gradle中添加依赖
dependencies {
compile 'com.alipay.euler:andfix:0.5.0@aar'
}
在Application中初始化AndFix
public class AndFixApplication extends Application {
public static PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
// 初始化patch管理类
mPatchManager = new PatchManager(this);
// 初始化patch版本
mPatchManager.init("1.0");
// String appVersion = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
// mPatchManager.init(appVersion);
// 加载已经添加到PatchManager中的patch
mPatchManager.loadPatch();
}
}
生成patch包
为了方便演示,我们设置点击按钮来加载patch
public class MainActivity extends AppCompatActivity {
private static final String APATCH_PATH = "/fix.apatch"; // 补丁文件名
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.load).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
update();
}
});
}
private void update() {
String patchFileStr = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
try {
AndFixApplication.mPatchManager.addPatch(patchFileStr);
} catch (IOException e) {
e.printStackTrace();
}
}
}
patch命令
- -f <new.apk> :新apk
- -t <old.apk> : 旧apk
- -o <output> : 输出目录(补丁文件的存放目录)
- -k <keystore>: 打包所用的keystore
- -p <password>: keystore的密码
- -a <alias>: keystore 用户别名
- -e <alias password>: keystore 用户别名密码
sh apkpatch.sh -f app-release-2.0.apk -t app-release-1.0.apk -o output -k abc.keystore -p qwe123 -a abc.keystore -e qwe123
运行
load patch
#安装应用
adb install app-debug-1.0.apk
#将patch push到手机中
adb push fix.apatch /storage/emulated/0/fix.apatch
原理解析
.apatch实际是一个压缩文件
Manifest-Version: 1.0
Patch-Name: app-debug-2
Created-Time: 12 May 2017 02:31:07 GMT
From-File: app-debug-2.0.apk
To-File: app-debug-1.0.apk
Patch-Classes: com.example.fensan.andfixdemo.MainActivity_CF
Created-By: 1.0 (ApkPatch)
这个Patch-CLasses标志了哪些类有修改,这里会显示完全的类名同时加上一个_CF后缀。AndFix首先会读取这个文件里面的东西,保存在Patch类的一个对象里,备用。
然后我们反编译diff.dex来查看里面的类,用jd-gui来查看:
可以看到这个dex里面只有一个class,而且在我们所修改的方法上有一个"@MethodReplace"注解,在代码中可以明显的看到了我们加入的this.fix ="修复了"
这段代码!
源码浅析
1. PatchManager
/**
* initialize
*
* @param appVersion
* App version
*/
public void init(String appVersion) {
//patch路径的初始化
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
//针对apk版本的patch处理,如果版本不一致清除,版本一直则加载。
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
//初始化本地的patch
initPatchs();
}
}
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
在init()方法中,主要对本地的patch进行处理,当apk的版本与patch的版本一致,就加载本地的patch;版本不一致则清除。
接下来我们来看loadPatch()的代码实现:
/**
* load patch,call when application start
*
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
//对本地的patch进行遍历,并fix
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
loadPatch()中对本地的patch进行了遍历,获取每个patch的信息,逐一进行fix(),其中参数classes为patch中配置文件Patch.MF的Patch-Classes字段对应的所有类,即为要修复的类。
接下来进入AndFixManager中。
AndFixManager
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
//得到类所有公用方法
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
//枚举方式,通过注解找到需要替换的类
for (Method method : methods) {
//获得打了@MethodRepalce标签的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
//需要替换的类,执行下一步
replaceMethod(classLoader, clz, meth, method);
}
}
}
/**
* replace method
*
* @param classLoader classloader
* @param clz class
* @param meth name of target method
* @param method source method
*/
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
//得到原apk中要替换的类
Class<?> clazz = mFixedClass.get(key);
//如果该类还没有加载
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
// 初始化该类
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
//开始进入native层进行方法的替换
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
fixClass()方法中进行的过程就是从需要修复的类中定位到需要修复的方法。
replaceMethod() 定位到需要修复的方法以后,进入AndFix进行方法的替换。
AndFix
AndFix是Java层进行方法替换的核心类,在该类中提供了Native层的接口,加载了andfix.cpp,主要进行了Native层的初始化,以及目标修复类的替换工作。
/**
* replace method's body
*
* @param src
* source method
* @param dest
* target method
*
*/
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
private static native void replaceMethod(Method dest, Method src);
Native层
Dalvik部分
extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
JNIEnv* env, int apilevel) {
//Davik虚拟机实现 是在libdvm.so中
//dlopen()方法以指定模式打开动态链接库,RTLD_NOW立即打开
void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
if (dvm_hand) {
//dvm_dlsym:通过句柄和连接符名称获取函数或变量名
dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ?
"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
"dvmDecodeIndirectRef");
if (!dvmDecodeIndirectRef_fnPtr) {
return JNI_FALSE;
}
dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
if (!dvmThreadSelf_fnPtr) {
return JNI_FALSE;
}
jclass clazz = env->FindClass("java/lang/reflect/Method");
jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
"()Ljava/lang/Class;");
return JNI_TRUE;
} else {
return JNI_FALSE;
}
}
该方法进行的操作主要是打开运行dalvik虚拟机的libdvm.so
,得到dvmDecodeIndirectRef_fnPtr、dvmThreadSelf_fnPtr
函数,下面将用到这两个函数获取类对象。
接下来我们进入整个AndFix最核心的dalvik_replaceMethod()
方法中,在其中进行了对类方法指针的替换,真正实现对方法的替换。
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
//clazz为被替换的类
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
//clz 为被替换的类对象
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
//将类状态设置为装载完毕
clz->status = CLASS_INITIALIZED;
//得到指向新方法的指针
Method* meth = (Method*) env->FromReflectedMethod(src);
//得到指向需要修复的目标方法的指针
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
//新方法指向目标方法,实现方法的替换
// meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}
ART
art部分根据版本号的不同,进行了不同的处理。
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
//获得指向新的方法的指针
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
//获得指向被替换的目标方法的指针
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
//目标方法的装载器和方法中声明类设置为新方法对应值
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_;
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
//新方法指向目标方法,实现方法的替换
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->access_flags_ = dmeth->access_flags_;
smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
smeth->dex_cache_initialized_static_storage_ =
dmeth->dex_cache_initialized_static_storage_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->vmap_table_ = dmeth->vmap_table_;
smeth->core_spill_mask_ = dmeth->core_spill_mask_;
smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
smeth->mapping_table_ = dmeth->mapping_table_;
smeth->code_item_offset_ = dmeth->code_item_offset_;
smeth->entry_point_from_compiled_code_ =
dmeth->entry_point_from_compiled_code_;
smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
smeth->native_method_ = dmeth->native_method_;
smeth->method_index_ = dmeth->method_index_;
smeth->method_dex_index_ = dmeth->method_dex_index_;
LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,
dmeth->entry_point_from_compiled_code_);
}
总体的过程总结如下:
- 初始化patch管理器,加载补丁;
- 检查手机是否支持,判断ART、Dalvik;
- 进行md5,指纹的安全检查
- 验证补丁的配置,通过patch-classes字段得到要替换的所有类
- 通过注解从类中得到具体要替换的方法
- 修改方法的访问权限为public
- 得到指向新方法和被替换目标方法的指针,将新方法指向目标方法,完成方法的替换。
AndFix提供了一种Native层hook Java层代码的思路,实现了动态的替换方法。在处理简单没有特别复杂的方法中有独特的优势,但因为在加载类时跳过了类装载过程直接设置为初始化完毕,所以不支持新增静态变量和方法。