112

Android多开检测的另一个思路

 5 years ago
source link: https://blog.darkness463.top/2018/09/30/Android-Virtual-Check-Plus/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

起因

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

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

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。

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 动态库。
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的动态库加载进去)的话,出现非自己包名的动态库可能疑似运行在多开环境下。服务端可以建立一套自动解析包名 + 添加到黑名单的流程。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK