深入探究按键精灵(Android)的实现原理

逆向工程

Posted by 木木的木头 on May 15, 2019

深入探究按键精灵(Android)的实现原理

起因:本来想用按键精灵写点挂机脚本,但广告过于恶心,于是就想自己写一个模拟人工操作的app,但是写的时候发现各种权限问题,于是便好奇想探究一下按键精灵是如何做到Root后后台截屏并模拟手动点击的

Step 1

首先是在网上查了按键精灵的相关讨论,发现挺多人其实都在研究按键精灵的实现,但是大家都是从结果讨论的,基本上想办法做到能够模拟出点击就结束了,并没有证明按键精灵究竟是如何实现的。

Step 2

根据网上相关的资料,大致有了几个未经验证的猜想。

猜想1:模拟点击实现都是使用Root权限模拟发送ADB的input命令。

猜想2:后台截图使用ADB的screencap命令。

猜想3:Android 5.0后使用MediaProjectionManager 的录屏接口进行截屏,但是这样的话会系统弹窗让用户确认授权。

Step 3

动手实践,上述3种方案我都写代码进行了尝试,最终证明上面3种方案都能实现相关功能,但是猜想2有验证的性能问题,猜想3无法解决弹窗授权问题,而按键精灵明明没有弹窗,只需要授予Root权限即可。

screencap在java代码中并不能直接像命令行那样拿到图片的二进制数据返回,而只能将截图保存到闪存中,然后再从闪存读取图片数据。这样虽然能够实现,但是性能显然太差,而且伤硬件。

猜想3使用了Google官方的Demo测试,性能没问题,但是授权必须用户确认。

Step 4

为了最终找到按键精灵的底层原理,我反编译了安卓版的按键精灵,一探究竟。工具:Luyten,Dex2Jar。另外,我刚开始读jar文件用的 jd-gui,但是部分文件发现读取失败,于是换了Luyten,Luyten效果好一点。反编译后全局搜索ScreenShot关键字,找到了一个ForScreenShotActivity类,这个就是按键精灵截图实现的Activity,很明显能够看到其中

 private void a() {
        if (Build$VERSION.SDK_INT >= 21) {
            this.startActivityForResult(((MediaProjectionManager)this.getSystemService("media_projection")).createScreenCaptureIntent(), 32896);
            return;
        }
        Log.e("ForScreenShotActivity", "The API version is too low,required is >= 21.");
        this.finish();
    }
    
    protected void onActivityResult(final int n, final int n2, final Intent intent) {
        super.onActivityResult(n, n2, intent);
        if (n != 32896) {
            return;
        }
        if (n2 == -1 && intent != null) {
            this.setResult(-1);
            ScreenShoterV3.getInstance().init((Context)this, intent);
            this.finish();
            return;
        }
        this.a();
        this.setResult(0);
    }
    
    protected void onCreate(final Bundle bundle) {
        super.onCreate(bundle);
        this.requestWindowFeature(1);
        this.getWindow().setBackgroundDrawable((Drawable)new ColorDrawable(0));
        this.getWindow().setDimAmount(0.0f);
        this.a();
    }

其中,”media_projection”刚好对于安卓的Context.MEDIA_PROJECTION_SERVICE常量值,而-1刚好对应Activity.RESULT_OK常量值。到此,如果是Android开发者肯定很清楚了,按键精灵的后台截图就是使用了录屏接口。不过为什么没有出现弹窗要求授权我猜测是他使用Root权限,直接修改了安卓的记录文件,相当于你点击了授权弹窗那里的“下次不再提醒”。

然后,继续找下去,最终,找到了一个Injector类,读了反编译源码后,发现了下面这段代码

                switch (message.what) {
                    default: {}
                    case 8: {
                        final Object obj = message.obj;
                        MqRunnerLite.getInstance();
                        MqRunner.getInstance();
                    }
                    case 7: {
                        Injector.a((byte[])message.obj);
                    }
                    case 6: {
                        MqRunnerLite.getInstance();
                    }
                    case 5: {
                        MqRunnerLite.getInstance();
                    }
                    case 4: {
                        final Object obj2 = message.obj;
                        MqRunnerLite.getInstance();
                    }
                    case 3: {
                        Injector.c((com.cyjh.event.b)message.obj);
                    }
                    case 2: {
                        Injector.b((com.cyjh.event.b)message.obj);
                    }
                    case 1: {
                        Injector.a((com.cyjh.event.b)message.obj);
                    }
                }

其中,每一项对应了一个事件,简单分析了下应该是触摸,拖动,按下,抬起….点击去看了具体实现,基本上每一项最终都会走到一个类似这样的函数

    public static void TouchDown(int a, final float n, final float n2) {
        try {
            if (Injector.r == null) {
                Injector.r = new a[5];
                for (int i = 0; i < 5; ++i) {
                    final a a2 = new a((byte)0);
                    a2.a = true;
                    a2.b = 0;
                    a2.c = 0.0f;
                    a2.d = 0.0f;
                    Injector.r[i] = a2;
                }
            }
            if (i() >= 5) {
                return;
            }
            a(a, n, n2);
            final long uptimeMillis = SystemClock.uptimeMillis();
            a = a(a, 1);
            final int j = i();
            j();
            g().sendPointerSync(MotionEvent.obtain(uptimeMillis, uptimeMillis, a, j, Injector.s, Injector.t, 0, 0, 0.0f, 0.0f, 0, 0, 0, 0));
        }
        catch (Throwable t) {
            t.toString();
        }
    }

而其中g()这个函数的实现是

    private static Instrumentation g() {
        if (Injector.b == null) {
            Injector.b = new Instrumentation();
        }
        return Injector.b;
    }

然后Google这个Instrumentation类,赫然写着,这个类是google底层提供的一个自动化测试类,通常情况下只能测试与自己包名相同的应用,但是!如果具有root权限,或者是系统签名的包,就可以对全部App生效!哈哈!至此,按键精灵全部底层实现都被我扒出来了。哈哈,接下来就是去撸一个自己的挂机脚本了,按键精灵再见。