Android 禁止应用多开

Android 禁止应用多开

本文转自 九音 并作补充

Android多开

原理

一种是从多开App中直接加载被多开的App,如平行空间、VirtualApp等,另一种是让用户新安装一个App,但这个App本质上就是一个壳,用来加载被多开的App,其原理和前一种是一样的,市面上多开分身这款App是用的这种形式,用户每分身一个App需新安装一个包名为dkmodel.xxx.xxx的App

Android检测方案

1、检查files目录路径

App的私有目录是/data/data/包名/或/data/user/用户号/包名通过Context.getFilesDir()方法可以拿到私有目录下的files目录。

但是在多开环境下,获取到目录会变为/data/data/多开App的包名/xxxxxxxx或/data/user/用户号/多开App的包名/xxxxxxxx。

示例:

正常使用App上面的代码获取到的路径:

/data/user/0/top.darkness463.virtualcheck/files。

多开路径:

/data/user/0/dkmodel.zom.rxo/virtual/data/user/0/top.darkness463.virtualcheck/files。

2、应用列表检测

应用列表检测不是指简单的遍历应用列表判断是不是安装了多开App,我们并不阻止用户安装多开App并多开其他App,我们只是不希望用户多开我们自己的App,因此不能检测到用户安装了多开App就把他干掉。

多开App都会对context.getPackageName()进行处理,让这个方法返回原始App的包名,因此在被多开的App看来,多开App的包名和原始的那个App的包名一样,因此在多开环境下遍历应用列表时会发现包名等于原始App的包名的应用会有两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean checkPkg(Context context){
try{
if (context == null){
return false;
}
int count = 0;
String packageName = context.getPackageName();
PackageManager pm = context.getPackageManager();
List<PackageInfo> pkgs = pm.getInstalledPackages(0);
for (PackageInfo info : pkgs){
if (packageName.equals(info.packageName)){
count++;
}
}
return count > 1;
} catch (Exception ignore){}
return false;
}

缺点:

只对部分多开App有效,例如360的分身大师,不少多开App会绕过这项检测

3、Maps检测

读取/proc/self/maps,多开App会加载一些自己的so到内存空间

比如说:

360的分身大师加载了其目录下的某个so,/data/app/com.qihoo.magic-gdEsg8KRAuJy0MuY18BlqQ==/lib/arm/libbreakpad-jni-1.5.so,通过对各种多开App的包名的匹配,如果maps中有多开App的包名的东西,那么当前就是运行在多开环境下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Set<String> virtualPkgs;
private boolean check(){
BufferedReader bufr = null;
try {
bufr = new BufferedReader(new FileReader("/proc/self/maps"));
String line;
while ((line = bufr.readLine()) != null){
for (String pkg : virtualPkgs){
if (line.contains(pkg)){
return true;
}
}
}
} catch (Exception ignore){}
finally {
if (bufr != null){
try {
bufr.close();
} catch (IOException e){}
}
}
return false;
}

缺点:

目前没有发现多开App绕过该项检测,但缺点是需要收集所有多开App的包名,一旦多开App改个包名就失效了。

4、ps检测

通过执行ps命令并以自己的uid进行过滤,得到类似下面的结果:

image

多开环境下:会获取到自己的包名和多开App的包名这2个包名,通过这些包名去/data/data/下找会找到2个目录

而正常情况下只能在/data/data/下找到自己的App的目录

具体方法网址:

(https://blog.csdn.net/shdhenghao3/article/details/94409299)

https://www.sohu.com/a/242918900_659256

四种方案测试结果

image

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

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

image

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

狭义多开

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

广义多开:

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

最终方案

第1步:扫描本地端口(扫描tcp文件并格式化端口的关键代码)

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
>String tcp6 =CommandUtil.getSingleInstance().exec("cat /proc/net/tcp6");

>if(TextUtils.isEmpty(tcp6))return;

String[] lines =tcp6.split("n");

ArrayListportList =newArrayList<>();

for(inti =0, len = lines.length; i < len; i++) {

intlocalHost = lines[i].indexOf("0100007F:");

//127.0.0.1:的位置

if(localHost <0)continue;

StringsinglePort = lines[i].substring(localHost +9, localHost +13);

//截取端口

Integer port =Integer.parseInt(singlePort,16);

//16进制转成10进制

portList.add(port);

}

第2步:发起连接请求

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

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
try{

//发起连接,并发送消息

Socket socket=newSocket("127.0.0.1",port);

socket.setSoTimeout(2000);

OutputStreamoutputStream = socket.getOutputStream();

outputStream.write((secret+"n").getBytes("utf-8"));

outputStream.flush();

socket.shutdownOutput();

//获取输入流,这里没做处理,纯打印

InputStreaminputStream = socket.getInputStream();

BufferedReaderbufferedReader =newBufferedReader(newInputStreamReader(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克隆体)。

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
privatevoidstartServer(String

secret){

Random random=newRandom();

ServerSocketserverSocket =null;

try{

serverSocket=newServerSocket();

serverSocket.bind(newInetSocketAddress("127.0.0.1",

random.nextInt(55534) +10000));

//开一个10000~65535之间的端口

while(true) {

Socket socket =serverSocket.accept();

ReadThreadreadThread =newReadThread(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本体发送的密文,这里是接收端主动运行一个空指针异常,杀死自己。处理方式有点像《三体》的黑暗森林法则,谁先暴露谁先死。

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
privateclassReadThreadextendsThread{

privateReadThread(String

secret, Socket socket){

InputStreaminputStream =null;

try{

inputStream =socket.getInputStream();

bytebuffer[] =newbyte[1024*4];

inttemp =0;

while((temp = inputStream.read(buffer)) !=-1) {

String result=newString(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权限,本库不会通过网络上传任何隐私

本文方案已经集成到

github地址:

https://github.com/lamster2018/EasyProtector