AndFix原理分析.md

作者: sanfen | 来源:发表于2017-09-04 10:31 被阅读514次

    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的时候打出了一段日志。

    hook

    AndFix使用

    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

    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
    
    patch_diff

    运行

    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来查看:

    image

    可以看到这个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部分根据版本号的不同,进行了不同的处理。

    image
    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_);
    
    }
    

    总体的过程总结如下:

    1. 初始化patch管理器,加载补丁;
    2. 检查手机是否支持,判断ART、Dalvik;
    3. 进行md5,指纹的安全检查
    4. 验证补丁的配置,通过patch-classes字段得到要替换的所有类
    5. 通过注解从类中得到具体要替换的方法
    6. 修改方法的访问权限为public
    7. 得到指向新方法和被替换目标方法的指针,将新方法指向目标方法,完成方法的替换。

    AndFix提供了一种Native层hook Java层代码的思路,实现了动态的替换方法。在处理简单没有特别复杂的方法中有独特的优势,但因为在加载类时跳过了类装载过程直接设置为初始化完毕,所以不支持新增静态变量和方法。

    源码传送门

    相关文章

      网友评论

        本文标题:AndFix原理分析.md

        本文链接:https://www.haomeiwen.com/subject/fxnsjxtx.html