一行代码帮你检测Android多开软件

640?wx_fmt=png


今日科技快讯


近日,国家药监局称查获一批长春长生生物科技有限责任公司生产记录造假的狂犬疫苗,已对其相关违法违规行为立案调查。根据媒体报道显示,包括长生生物、武汉生物、深圳泰康等多家疫苗公司在内,都由同样的三个人控股,并多次出现涉嫌造假问题。对此京东创始人刘强东表示:我们的孩子打的90%是这几个公司的疫苗,这种人至少该判无期不得假释!


作者简介


新的一周开始,很高兴又跟大家见面啦!

本篇来自 普通的程序员 的投稿,分享了检测多开软件的方法,希望能够帮助大家!

普通的程序员 的博客地址:

https://www.jianshu.com/u/38b409cda5af


简介


最近有业务上的要求,要求app在本地进行诸如软件多开、hook框架、模拟器等安全检测,防止作弊行为。

防作弊一直是老生常谈的问题,而软件多开检测往往是防作弊中的重要一环,在查找资料的过程中发现多开软件公司对防多开手段进行了针对性的升级,即使非常新的资料也无法做到通杀。

所以站在前人的肩膀上,继续研究。


借鉴方案


借鉴方案来自以下两个帖子

《Android多开/分身检测》

https://blog.darkness463.top/2018/05/04/Android-Virtual-Check/

《Android虚拟机多开检测》

https://www.jianshu.com/p/216d65d9971e

文中的方案简单总结起来是4点:

1. 私有文件路径检测;

2. 应用列表检测;

3. maps检测;

4. ps检测;

代码此处不贴了,这四种方案测试结果如下:

640?wx_fmt=png

测试方案顺序1234,测试结果X代表未能检测O成功检测多开;

virtual app测试版本是git开源版,商用版已经修复uid的问题;

可以看到的是,检测效果不是很理想,没有哪一种方法可以做到通杀市面排名靠前的这些多开软件,甚至在高版本机器上,多开软件完美避开了检测。


端口监听法思路


为了避免歧义,我们接下来所说的app都是指的同一款软件,并定义普通运行的app叫做本体,运行在多开软件上的app叫克隆体。并提出以下两个概念

狭义多开:只要app是通过多开软件打开的,则认为多开,即使同一时间内只运行了一个app

广义多开:无论app是否运行在多开软件上,只要app在运行期间,有其余的『自己』在运行,则认为多开

(有点《第六日》的意思,克隆人以为自己是真人,发现跟自己一模一样的人,都认为对方是克隆人)

我们前面所借鉴的四种方案,都是去针对狭义多开进行检测,通过判断运行在多开软件时的特征进行反制,多开软件也会针对这些检测方案进行研究,提出相应措施。

那么我们退一步,顺着检测广义多开的方向进行思考,我们允许app运行在多开软件上,但是在一台机器上同一时间有且只能运行一个app(无论本体or克隆体),只要app能发现有一个同样的自己,然后干掉对方或自杀,就达到防止广义多开的目的。

那么我们怎样让这两个app见面呢?

微信同一账号不能同时登录在不同的手机上,靠的是网络请求,限定登录设备。

640?wx_fmt=png

那在本地如何处理这种情况呢?是不是也可以靠网络通信的方式完成见面?

答案当然是肯定的啊,不然我写这篇干嘛,利用socket,自己既当客户端又当服务端就能完成我们的需求。

640?wx_fmt=png

自己做服务端又做客户端

1. app运行后,先做发送端,在合适的时候去连接本地端口并发送一段密文消息,如果有端口连接且密文匹配,则认为之前已经有app在运行了(广义多开),接收端进行处理;

2. app再成为接收端,接收可能到来连接;

3. 后续若有app启动(无论本体or克隆体),则重复1&2步骤,达到『同一时间只有一个app在运行』的目的,解决广义多开的问题。


实现方案


思路有了,接下来就是实现,完整代码地址见文章底部。

第1步:扫描本地端口

想当然利用netstat指令来扫描已经开启的本地端口

640?wx_fmt=png

但是这个方法有3个坑:

1. netstat在部分机器上用不了(http://410063005.iteye.com/blog/1923543

2. busybox 在部分机器用不了;

640?wx_fmt=png

一加5T没有busybox工具

3. netstat的输出从源码上看,实际是纯打印;(https://blog.csdn.net/earbao/article/details/32191607

既然有这些坑,干脆直接手动处理,因为 netstat 的本质上还是去读取/proc/net/tcp等文件再格式化处理,tcp文件格式也是很标准化的,通过研究源码,找出端口之间的关系。
0100007F:8CA7 其实就是 127.0.0.1:36007

640?wx_fmt=png

/proc/net/tcp6文件

最终扫描tcp文件并格式化端口的关键代码

String tcp6 = CommandUtil.getSingleInstance().exec("cat /proc/net/tcp6");
     if (TextUtils.isEmpty(tcp6)) return;
     String[] lines = tcp6.split("\n");
     ArrayList<Integer> portList = new ArrayList<>();
     for (int i = 0, len = lines.length; i < len; i++) {
       int localHost = lines[i].indexOf("0100007F:");
       //127.0.0.1:的位置
       if (localHost < 0continue;
       String singlePort = lines[i].substring(localHost + 9, localHost + 13);
       //截取端口
       Integer port = Integer.parseInt(singlePort, 16);
       //16进制转成10进制
       portList.add(port);
}
第2步:发起连接请求

接下来向每个端口都发起一个线程进行连接,并发送自定义消息,该段消息用app的包名就行了(多开软件很大程度会hook getPackageName方法,干脆就顺着多开软件做)

try {
       //发起连接,并发送消息
       Socket socket = new Socket("127.0.0.1", port);
       socket.setSoTimeout(2000);
       OutputStream outputStream = socket.getOutputStream();
       outputStream.write((secret + "\n").getBytes("utf-8"));
       outputStream.flush();
       socket.shutdownOutput();
       //获取输入流,这里没做处理,纯打印
       InputStream inputStream = socket.getInputStream();
       BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
       String info = null;
       while ((info = bufferedReader.readLine()) != null) {
         Log.i(TAG, "ClientThread: " + info);
       }

       bufferedReader.close();
       inputStream.close();
       socket.close();
     } catch (ConnectException e) {
       Log.i(TAG, port + "port refused");
 }

主动连接的过程完成,先于自己启动的app(可能是本体or克隆体)接收到消息并进行处理。

第3步:成为接收端,等待连接

接下来就是成为接收端,监听某端口,等待可能到来的app连接(可能是本体or克隆体)。

private void startServer(String secret) {
     Random random = new Random();
     ServerSocket serverSocket = null;
     try {
       serverSocket = new ServerSocket();
       serverSocket.bind(new InetSocketAddress("127.0.0.1",
       random.nextInt(55534) + 10000));
      //开一个10000~65535之间的端口
       while (true) {
         Socket socket = serverSocket.accept();
         ReadThread readThread = new ReadThread(secret, socket);
        //假如这个方案很多app都在用,还是每个连接都开线程处理一些
         readThread.start();
//                serverSocket.close();
         }
     } catch (BindException e) {
       startServer(secret);//may be loop forever
     } catch (IOException e) {
       e.printStackTrace();
     }
 }

开启端口时为了避免开一个已经开启的端口,主动捕获BindExecption,并迭代调用,可能会因此无限循环,如果怕死循环的话,可以加一个类似ConcurrentHashMap最坏尝试次数的计数值。不过实际测试没那么衰,随机端口范围10000~65535,最多尝试两次就好了。

每一个处理线程,做的事情就是匹配密文,对应上了就是某个克隆体or本体发送的密文,这里是接收端主动运行一个空指针异常,杀死自己。处理方式有点像《三体》的黑暗森林法则,谁先暴露谁先死。

private class ReadThread extends Thread {
       private ReadThread(String secret, Socket socket) {
       InputStream inputStream = null;
       try {
         inputStream = socket.getInputStream();
         byte buffer[] = new byte[1024 * 4];
         int temp = 0;
         while ((temp = inputStream.read(buffer)) != -1) {
         String result = new String(buffer, 0, temp);
         if (result.contains(secret)) {
//                        System.exit(0);
//                        Process.killProcess(Process.myPid());
                   nullPointTV.setText("");
               }
           }
       inputStream.close();
       socket.close();
       } catch (IOException e) {
       e.printStackTrace();
      }
     }
 }

*因为端口通信需要Internet权限,本库不会通过网络上传任何隐私


测试结果


以之前提到的那些机型和多开软件做测试样本,目前测试效果基本做到通杀。因安卓机型太广,真机覆盖测试不完全,有空大家去git提issue;

在application的mainProcess里调用一次即可。模拟器因为会抢localhost,demo里做了模拟器判断。

本文方案已经集成到

EasyProtectorLib

https://jcenter.bintray.com/com/lahm/library/easy-protector-release/

github地址:

https://github.com/lamster2018/EasyProtector

中文文档见:

https://www.jianshu.com/p/c37b1bdb4757

使用方法 VirtualApkCheckUtil.getSingleInstance().checkByPortListening(String secret);

Todo

1. 检测到多开应该提供回调给开发者自行处理;

2. 同样的思路,利用ContentProvider也应该可以完成


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

640.png?

640?wx_fmt=jpeg

猜你喜欢

转载自blog.csdn.net/c10wtiybq1ye3/article/details/81161462