Android多开检测的另一个思路

之前写了篇检测多开的文章后,经过几个月的时间,基本上都已经被各多开软件绕过了。最近无意中发现了一些新特征,在特定环境下可以用来检测多开环境,特此来分享一下。

起因

某次在多开环境下运行demo,发现动态库加载失败了,错误信息如下:

1
2
3
4
java.lang.UnsatisfiedLinkError: dlopen failed: "/data/app/【手动打码】/lib/arm64/【手动打码】.so" is 64-bit instead of 32-bit
 at java.lang.Runtime.loadLibrary0(Runtime.java:1016)
 at java.lang.System.loadLibrary(System.java:1660)
 ... 省略

这个错误没什么好多说的,很明显,动态库是64位的而App运行在32位下,因此加载失败了。其实之前也看到过类似的现象,本应在64位下运行的App到多开环境下就变为32位环境了,只是之前并没有去深究,这次遇到后仔细想了一下,在某些情况下可以用来检测多开环境。

解释

对于64位的手机,会启动2个zygote,zygotezygote64

1
2
root  670 1 4359784  27292 poll_schedule_timeout 7f7b47058c S zygote64
root  671 1 1696576  11120 poll_schedule_timeout eb823684   S zygote

以64位运行的App将由zygote64 fork而来,而以32位运行的App将由zygote fork出来。可以从下面的代码看出来,ZygoteProcess.java中,startViaZygote方法会通过openZygoteSocketIfNeeded方法选择合适的zygote。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private Process.ProcessStartResult startViaZygote(final String processClass,
                                                  final String niceName,
                                                  final int uid, final int gid,
                                                  final int[] gids,
                                                  int debugFlags, int mountExternal,
                                                  int targetSdkVersion,
                                                  String seInfo,
                                                  String abi,
                                                  String instructionSet,
                                                  String appDataDir,
                                                  String invokeWith,
                                                  String[] extraArgs)
        throws ZygoteStartFailedEx {
    // 省略...
    synchronized (mLock) {
        return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
    }
}

@GuardedBy("mLock")
private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
    Preconditions.checkState(Thread.holdsLock(mLock), "ZygoteProcess lock not held");

    if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
        try {
            primaryZygoteState = ZygoteState.connect(mSocket);
        } catch (IOException ioe) {
            throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
        }
    }

    if (primaryZygoteState.matches(abi)) {
        return primaryZygoteState;
    }

    // The primary zygote didn't match. Try the secondary.
    if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
        try {
            secondaryZygoteState = ZygoteState.connect(mSecondarySocket);
        } catch (IOException ioe) {
            throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
        }
    }

    if (secondaryZygoteState.matches(abi)) {
        return secondaryZygoteState;
    }

    throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
}

那么如何确定App是以64位还是以32位运行呢?这就取决于动态库,如果只有32位的动态库(armeabi、armeabi-v7a),那么就会以32位运行,如果有64位的动态库(arm64-v8a),那么就以64位运行,不存在动态库则默认以64位运行。

如何证明?我们写3个demo来实际看一下。

  • demo1: 包名top.darkness463.whichzygote,不加动态库。
  • demo2: 包名top.darkness463.zygote32,加一个armeabi动态库。
  • demo3: 包名top.darkness463.zygote64,加armeabiarm64-v8a动态库。
1
2
3
4
5
root     670    1   4359784  27692 poll_schedule_timeout 7f7b47058c S zygote64
root     671    1   1696576  11416 poll_schedule_timeout eb823684   S zygote
u0_a177  9878   670 4456080  63412 SyS_epoll_wait        7f7b47046c S top.darkness463.whichzygote
u0_a180  13690  671 1791700  58664 SyS_epoll_wait        eb8234ac   S top.darkness463.zygote32
u0_a179  13871  670 4456524  64716 SyS_epoll_wait        7f7b47046c S top.darkness463.zygote64

可以看到,demo1和demo3的父进程是zygote64,而demo2的父进程是zygote

那么为何在多开环境下会出现动态库加载失败的情况呢?原因就在于我那个demo有arm64-v8a的动态库,在安装时,系统会把该64位动态库拷到/data/app/【包名】/lib/arm64/下,然后那款多开软件只有32位的动态库,因此是以32位运行的,此时去/data/app/【包名】/lib/arm64/路径下加载64位的动态库必然导致失败。

打开思路的话,这也可以是一种检测多开环境的方式。

局限性

其实与其说这是检测多开的方法,倒不如说这是多开软件的bug。看了几款排名靠前的多开软件,都只有32位的动态库,但它们完全可以加上64位动态库来避免这个问题。

另外,为了减小apk的体积,绝大多数App只会添加armeabi平台,而不会添加arm64-v8a平台的动态库,所以这种检测方法在很多App上本身就是不成立的。

补遗

之前那篇文章提到过一个通过/proc/self/maps来检测多开的方式,当时提到这个方法的缺点是需要收集所有多开App的包名,但真正搞事的人很可能不会拿市面上的多开软件来作恶,他们可能利用开源的多开软件改成乱七八糟的包名,之前我甚至见过命名成com.tencent.qqlite来进行伪装的。之后我又做了一些工作,也和大家分享一下。

思路还是从/proc/self/maps中的动态库出发。在这里直接给出结论了不再详细讨论了。

  • /proc/self/maps中出现包含 /vbox/data/、 /shadow/data/、 /virtual/data/的动态库,则运行在多开环境下。主要是因为很多多开软件都是基于开源或者抄来抄去的,所有目录名无外乎这么几种,但不排除会有多开软件修改掉名字的情况。
  • /proc/self/maps加载的动态库路径我们可以解析到包名,如果自己的App并不会加载其他App的动态库(第三方登录可能会把其他App的动态库加载进去)的话,出现非自己包名的动态库可能疑似运行在多开环境下。服务端可以建立一套自动解析包名 + 添加到黑名单的流程。

最后给大家分享一份非常系统和全面的Android进阶技术大纲及进阶资料,及面试题集

想学习更多Android知识,请加入Android技术开发交流 7520 16839

进群与大牛们一起讨论,还可获取Android高级架构资料、源码、笔记、视频

包括 高级UI、Gradle、RxJava、小程序、Hybrid、移动架构、React Native、性能优化等全面的Android高级实践技术讲解性能优化架构思维导图,和BATJ面试题及答案!

群里免费分享给有需要的朋友,希望能够帮助一些在这个行业发展迷茫的,或者想系统深入提升以及困于瓶颈的

朋友,在网上博客论坛等地方少花些时间找资料,把有限的时间,真正花在学习上,所以我在这免费分享一些架构资料及给大家。希望在这些资料中都有你需要的内容。

Android多开检测的另一个思路

Android多开检测的另一个思路