AndFix原理分析.md

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层代码的思路,实现了动态的替换方法。在处理简单没有特别复杂的方法中有独特的优势,但因为在加载类时跳过了类装载过程直接设置为初始化完毕,所以不支持新增静态变量和方法。

源码传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容