Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示

又是快一个周过去啦~不知道小伙伴们学习的如何,我们在上篇的博文中学了相关动态拦截修改的具体操作和应用,学习过后相信你对高能力高技能利用Xposed有了一个深入的理解。在我们前几篇的学习中,我们主要是结合Java的反射机制来配合Xposed实现神技能,那么今天的学习就没有了Java反射机制的配合,正如本篇博客的标题所示,在Xposed中弹出Toast提示~

关于Toast想必大家已经很熟悉了,主要用于一个短时间内的提示作用,作为一名Android开发人员来说,弹一个Toast提示已经如同家常便饭那样简单随心所欲。那么对于我们逆向工程师而言,如果有一个需要要求我们在脚本代码中需要弹出一个Toast给用户提示,面对和应用层活动不同的运行环境,我们该如何做到像开发人员那样随心所欲的弹Toast呢?

那么就让我们开始今天的学习吧~本次的教案对象还是腾讯旗下的产品:应用宝,还是熟悉的配方熟悉的味道,依旧7.2.6版本呦~你可能会说,怎么还是使用7.2.6版本?那么我就还要忍不住夸奖应用宝同学了,为了应用的安全,应用宝一直在努力快速地进行产品的更新升级换代,目前最新的版本是7.2.9,有兴趣的同学可以查一下它的产品更新速度,绝对让你惊呼不已,至少在博主看来,算得上更新最快的App了~为什么博主一直坚持用7.2.6版本做教案对象,就是因为应用宝的源代码每一版本都是做出过改动的!适用于7.2.6版本的脚本代码却不会适用7.2.7版本!而博主不想费劲更新一个再去破解另一个....太累了有没有!

从这几天我们的学习中,透过应用宝我们就可以学到很多如何提高安全防护的知识,那么本次博主点评的应用宝安全策略主要是:产品快速的更新换代!关键代码的更替修改!这对于应用的安全同样至关重要。好了点评结束,再次感谢应用宝的辛苦付出,我们的口号是只学习技术,搞破坏是不可以哒!下面就开始今天的学习吧!

老样子,如果想在Xposed中实现弹出一条Toast,那么我们势必要去分析了解Toast的具体工作详情。在开发过程中,我们使用下面一句代码来实现:

Toast.makeText(this,"xxxxxx",Toast.LENGTH_SHORT).show();

非常简单,直接调用Toast类中的makeText()静态方法,传入三个参数,然后调用show()方法,文本就会在界面中弹出来。我们发现主要方法为makeText()方法,它的三个参数分别为Context,需要弹出的文本信息,一个Int型静态变量,并没有什么特殊的东西,那么分析,如果我们在Xposed的Hook方法中直接调用makeText()方法来弹Toast是不是也是可行的呢?答案是当然可行的,既然如此那么我们就去兴致勃勃的在Hook方法中写上代码:Toast.makeText(this,"xxxxxx",Toast.LENGTH_SHORT).show();,但是很快你就发现有点困难了,Context你拿不到!

难点来了,如何解决Context问题!Context想必大家都很熟悉也很了解了,重要性和Activity差不多了。在开发过程中,我们很轻而易举的就会拿到当前Context,毕竟开发所处在应用层,直接一个this指针就拿到了。但是在Xposed层面下,拿到Context却不会是那么的容易,因为所处的运行层面不同,使得获取Context还需要一番脑筋!

要想知道如何拿到Context,那么我们就需要深入了解一下应用层,也就是Activity是如何启动的。关于Activity的启动过程不知道你是否进行过深入的了解,貌似我们大多数都熟悉的是一个活动被创建是从onCreate()方法开始的,但是准确来说,onCreate()方法执行的是View的创建开始,真正意义上并不代表着一个Activity的创建,毕竟一个Activity所包括的不只View而已。那么在onCreate()方法之前,程序又做了哪些工作呢?

首先一个Activity的开始是从ActivityThread的main()方法开始的,在main()方法中启用Handle用来发送一条创建Activity消息,接收到消息后开始执行handleLaunchActivity()方法,接着又会调用performLaunchActivity()方法。以上main()方法,handleLaunchActivity()方法在这里不会再进行细致的分析,感兴趣的小伙伴可以自行看源码或者网上搜索相关的博文进行了解,这里关注点不同不做解释。我们主要从performLaunchActivity()方法进行细致的源码分析:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");

        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }

        ComponentName component = r.intent.getComponent();
        if (component == null) {
            component = r.intent.resolveActivity(
                mInitialApplication.getPackageManager());
            r.intent.setComponent(component);
        }

        if (r.activityInfo.targetActivity != null) {
            component = new ComponentName(r.activityInfo.packageName,
                    r.activityInfo.targetActivity);
        }

        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());
            r.intent.setExtrasClassLoader(cl);
            r.intent.prepareToEnterProcess();
            if (r.state != null) {
                r.state.setClassLoader(cl);
            }
        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to instantiate activity " + component
                    + ": " + e.toString(), e);
            }
        }

        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

            if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
            if (localLOGV) Slog.v(
                    TAG, r + ": app=" + app
                    + ", appName=" + app.getPackageName()
                    + ", pkg=" + r.packageInfo.getPackageName()
                    + ", comp=" + r.intent.getComponent().toShortString()
                    + ", dir=" + r.packageInfo.getAppDir());

            if (activity != null) {
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                if (r.overrideConfig != null) {
                    config.updateFrom(r.overrideConfig);
                }
                if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                        + r.activityInfo.name + " with config " + config);
                Window window = null;
                if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                    window = r.mPendingRemoveWindow;
                    r.mPendingRemoveWindow = null;
                    r.mPendingRemoveWindowManager = null;
                }
                appContext.setOuterContext(activity);
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);

                if (customIntent != null) {
                    activity.mIntent = customIntent;
                }
                r.lastNonConfigurationInstances = null;
                checkAndBlockForNetworkAccess();
                activity.mStartedActivity = false;
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }

                activity.mCalled = false;
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
                if (!activity.mCalled) {
                    throw new SuperNotCalledException(
                        "Activity " + r.intent.getComponent().toShortString() +
                        " did not call through to super.onCreate()");
                }
                r.activity = activity;
                r.stopped = true;
                if (!r.activity.mFinished) {
                    activity.performStart();
                    r.stopped = false;
                }
                if (!r.activity.mFinished) {
                    if (r.isPersistable()) {
                        if (r.state != null || r.persistentState != null) {
                            mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
                                    r.persistentState);
                        }
                    } else if (r.state != null) {
                        mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
                    }
                }
                if (!r.activity.mFinished) {
                    activity.mCalled = false;
                    if (r.isPersistable()) {
                        mInstrumentation.callActivityOnPostCreate(activity, r.state,
                                r.persistentState);
                    } else {
                        mInstrumentation.callActivityOnPostCreate(activity, r.state);
                    }
                    if (!activity.mCalled) {
                        throw new SuperNotCalledException(
                            "Activity " + r.intent.getComponent().toShortString() +
                            " did not call through to super.onPostCreate()");
                    }
                }
            }
            r.paused = true;

            mActivities.put(r.token, r);

        } catch (SuperNotCalledException e) {
            throw e;

        } catch (Exception e) {
            if (!mInstrumentation.onException(activity, e)) {
                throw new RuntimeException(
                    "Unable to start activity " + component
                    + ": " + e.toString(), e);
            }
        }

        return activity;
    }

以上就是performLaunchActivity()方法的源码,源码比较长,我们先从他的返回值看,返回值类型为Activity,从这里我们了解到performLaunchActivity()方法的功能就是用来组建或者装配一个Actvity,忽略一下开头代码,我们主要看下面两句代码:

Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示_第1张图片

首先都用方法createBaseContextForActivity()方法创建一个ContextImpl对象。 ContextImpl类是什么鬼?看下他的源代码就清楚了,他继承自抽象类Context类,主要实现Context类中的抽象方法,开发人员广为熟知的getApplicationContext()方法的实现,实际上就是调用的ContextImpl类中的getApplicationContext()。从源码中我们还可以得出一个结论就是,Context的创建要早于Activity。紧接着就声明了一个Activity对象,通过调用Instrumentation类中的newActivity()方法来创建一个Activity实例。

Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示_第2张图片

 从上面的newActivity()方法源码就可以看到,这个方法逻辑很简单,只是单纯的调用Class.newInstance()方法来创建一个对象,然后强转为Activity类型。所以说这个时候虽然创造出了Activity的实例,但是还是无法使用的,就像是一个初级产品,还需要进行一系列的装配才行,那么就继续分析代码:

Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示_第3张图片

这一部分代码的功能是获取各种数据,然后调用Activity类中的attach()方法,把获取到的数据传入进行装配,我们可以看一下attach()方法的源码:

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        mWindow.setWindowControllerCallback(this);
        mWindow.setCallback(this);
        mWindow.setOnWindowDismissedCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);
        if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
            mWindow.setSoftInputMode(info.softInputMode);
        }
        if (info.uiOptions != 0) {
            mWindow.setUiOptions(info.uiOptions);
        }
        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mReferrer = referrer;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;
        if (voiceInteractor != null) {
            if (lastNonConfigurationInstances != null) {
                mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
            } else {
                mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
                        Looper.myLooper());
            }
        }

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;

        mWindow.setColorMode(info.colorMode);
    }

 从源码中我们就可以看出确实进行了数据的装配,赋值等操作,比如设置界面元素值等操作。回想一下View的三步骤:measure、layout和draw,其实这里进行的就是measure测量过程。

分析到这里,我们停下来不再分析后续代码,如果想更深入的了解后续执行详情,请自行分析源码或者网上搜索相关的分析帖。回想下我们的目标,我们主要是解决如何在Xposed中拿到Context的问题。下面我们就正式分析下如何拿到Context!

解决思路:Xposed外部声明一个Context对象,然后通过拦截一个参数或者返回值类型为Context的方法为我们的Context对象赋值,这样就可以拿取到Context实例了。

按照我们的解决思路,我们首先把解决目光放至在attach()方法上,因为它的参数中有一个是Context 类型变量~不过拦截attach()方法不是最佳的选择,因为它的参数实在是太多啦!多到你怀疑人生.....那么就放弃attach()方法,哎哎等会....我们这时候把目光转到attach()方法内部,我们发现这样一句代码:attachBaseContext(context);

调用了 attachBaseContext()方法把传入的Context参数变量传了进去,这个attachBaseContext()方法看上去是一个很合适的拦截人选呀,那么我们去看看它源码是啥:

 Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示_第4张图片

Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示_第5张图片

没啥好说的,该方法是Activity的父类ContextThemeWrapper中的方法。Activity和Context的联系也从这里开始,有兴趣的小伙伴可以自行查找搜索Activity和Context的关系,Context算得上Activity的爷爷的爸爸,也就是太爷~怪不得我们在活动中使用this指针可以代表一个Context!

到这里分析工作其实就已经差不多了,我们明确了一个目标类:ContextThemeWrapper,目标方法为:attachBaseContext(),下面我们就可以写Hook代码啦,上代码:

 XposedHelpers.findAndHookMethod(ContextThemeWrapper.class, "attachBaseContext",Context.class, new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                context=(Context) param.args[0];
            }
        });
    

代码逻辑其实很简单,我们首先声明了一个Context型的变量,变量名为context,初始值为null,进入到拦截函数中内,使用参数变量为context变量赋值,这样就拿到了本次活动的Context。接下来就是使用该Context实例进行弹Toast,这时候要注意一点的是你的使用场景,也就是说保证该context使用的场景为Activity活动内,如果此时程序不是处在活动中,那么你的Toast可是弹不出来的,毕竟只有在活动内才可以弹出Toast。

那么下面我们就需要找到一个活动场景的某方法,可以拦截onCreate()方法,毕竟这个方法在活动中最为突出~这里博主具体就不再带领大家分析应用宝的源码找活动了,选出一个直接看效果吧,Xposed代码为:

XposedHelpers.findAndHookMethod("com.tencent.pangu.component.CommentDetailView", loadPackageParam.classLoader, "g",  new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
              
               if(context!=null){
                     Toast.makeText(context,"未登录,开始登录",Toast.LENGTH_SHORT).show();
                 }  
            }
        });

这里是拦截到应用宝内某活动的方法后,首先判断context是否为null,不为空则调用Toast的makeText()方法来弹出一条Toas提示,很简单的代码,让我们看一下效果:

Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示_第6张图片

 

确实实现了预期目标,弹出了一条Toast,提示“未登录,开始登录”!

总结本次难点,主要在于如何在Xposed中拿到Context,这里面涉及到Activity的启动过程源码分析,此外基本没有难点,拿到Context就可以弹出Toast了!此外还需要注意的一点是弹出Toast的时机,在Activity活动周期内进行弹出操作。

好了本篇博文到此结束,有不清楚的地方请评论留言告诉我,我会一一为你解答!有需要引用本文的地方请声明出处,谢谢!

 

你可能感兴趣的:(Android逆向工程:大显神通的Xposed,如何在Xposed中弹出Toast提示)