首页
社区
课程
招聘
11
[原创]Android 应用多开对抗实践
发表于: 2019-10-25 18:29 35256

[原创]Android 应用多开对抗实践

2019-10-25 18:29
35256

Android 应用多开对抗实践

白天午睡梦到些以前给某行业安全峰会写了材料,醒来后把记得的部分重新整理一下,分享出来给大家,尽量写得简洁明了。

 

未必准确,仅供参考,欢迎纠正补充

 

目录

应用多开技术总结

系统级技术

多开技术方案 发行版 APP
Android 多用户功能 OEM系统自带的"手机分身"、"应用双开",和 "Island/炼妖壶" 等各种 "Android for work" 产品
chroot 暂无发现 APP
  • 多用户功能

    多用户模式主要用到 UserManager 相关类,切换不同的用户,在不同的用户下运行 App,实现多开。最直观的例子是 Android 手机上的 多用户 功能, 手机分身 功能,以及 am switch-user 命令,这种简单粗暴的用法会将 Android 服务都运行一份,如果只用于应用多开,且不说资源消耗,切换用户是在麻烦。

    在 Android 5.0 在基于多用户功能 添加了 Android for work 功能,可以在同一个桌面启动器下使用受限用户启动 APP,不再需要切换界面。同时将权限开发给了非系统应用。

  • chroot

    UNIX 的 chroot 系统调用在 Android 上也能用,需要 root 权限。在本地挂载运行精简版系统镜像,使用远程桌面软件如 VNC 等访问本地多开的系统。尚未发现发行版 APP,可能在 ARM 服务器云手机中用到。

用户级技术

多开技术方案 发行版 APP
VirtualApp VirtualXposed, DualSpace
MultiDroid LBE平行空间, Parallel Space
DroidPlugin 分身大师
Excelliance 双开助手, MultiAccount
其它 虚拟大师
 

在用户级的多开技术中,还可以在按设计用途划分出三类

  • “容器”:VirtualApp、MultiDroid
  • 热更新/插件化:DroidPlugin、Excelliance
  • 虚拟系统:虚拟大师

具体实现原理大家可以翻论坛里的 精品贴,这里不多描述。

 

值得一提的是,某云手机团队的 "虚拟大师" 产品,实现在用户态运行了一个精简版的 Android 系统镜像,在系统库中拦截了几乎所有系统调用,使用类似前文提到的 chroot 挂载系统镜像的方法运行,有兴趣的同学可以看一看。

拆招

反系统级应用多开

仅多用户方案的多开,忽略 chroot /lxc

简单粗暴的代码

// --- C++ ---
#include 
bool isDualApp(){return 0 != getuid()/100000;}
1
2
3
// --- Java ---
import android.os.Process;
static boolean isDualApp(){return 0 != Process.myUid() / 100000;}

一行代码完事了?

 

完事了,真的完事了。

 

但是为什么?

 

Android 系统中,如果开启了多用户模式,会存在一个主用户和若干受限用户。

把 MIUI 的 "手机分身" 和 "应用双开" 功能都打开,可以看到有三用户,0、11 和 999,分别对应主用户、"手机分身" 和 "应用双开"

1
2
3
4
5
6
7
8
# --- adb shell ---
$ ls -al /data/user/
total 52
drwx--x--x   4 system system  4096 2019-09-05 11:49 .
drwxrwx--42 system system  4096 2019-04-22 20:32 ..
lrwxrwxrwx   1 root   root      10 1970-08-23 18:57 0 -> /data/data
drwxrwx--x 221 system system 16384 2019-09-05 11:50 11
drwxrwx--13 system system 16384 2019-09-12 17:53 999
 

使用多用户模式实现的多开,在客户端中可以通过 Android SDK 的 UserManger 类判断当前运行 APP 的用户是否为主用户和受限用户

1
2
3
4
5
6
7
8
9
// android.os.UserManger.java   
public boolean isAdminUser() {
    return isUserAdmin(UserHandle.myUserId());
}
// ...
public boolean isPrimaryUser() {
    UserInfo user = getUserInfo(UserHandle.myUserId());
    return user != null && user.isPrimary();
}

顺着线索,找到 UserHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// android.os.UserHandle.java
@Deprecated
/**
* @hide A user id constant to indicate the "owner" user of the device
* @deprecated Consider using either {@link UserHandle#USER_SYSTEM} constant or
* check the target user's flag {@link android.content.pm.UserInfo#isAdmin}.
*/
@Deprecated
public static final @UserIdInt int USER_OWNER = 0;
// ...
public static final @UserIdInt int USER_SYSTEM = 0;
// ...
public static final int PER_USER_RANGE = 100000;
// ...
public static @UserIdInt int getUserId(int uid) {
    if (MU_ENABLED) {
        return uid / PER_USER_RANGE;
    } else {
        return UserHandle.USER_SYSTEM;
    }
}

可看到通过 uid/100000 获得 Android 的 UserId,同时通过其它 final 字段和相关注释得知 OWNER/SYSTEM/ADMIN 的 UserId 是 0,因此我们可以通过 uid/100000 为 0 判断为主用户,非主用户直接判为多开即可。

验证

使用上文提到的 MIUI 中的 "应用双开" 功能,在进程中找到 UserId 999 运行的进程,因为第一列显示成了用户名,进 /proc/${PID}/status 查看进程 uid。uid / 100000 是 999,没毛病。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# --- adb shell ---
$ ps -ef | grep u999
u999_a118    14392   905 0 14:07:52 ?     00:00:01 com.miui.analytics
u999_system  19793   905 0 19:55:24 ?     00:00:00 com.android.keychain
shell        20408 13712 3 19:58:11 pts/0 00:00:00 grep u999
$ cat /proc/14392/status
Name:   .miui.analytics
Umask:  0077
State:  S (sleeping)
Tgid:   14392
Ngid:   0
Pid:    14392
PPid:   905
TracerPid:      0
Uid:    99910118        99910118        99910118        99910118
Gid:    99910118        99910118        99910118        99910118

可改进

  • 区分各种系统级双开/分身模式

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 11
支持
分享
赞赏记录
参与人
雪币
留言
时间
sinker_
感谢你的积极参与,期待更多精彩内容!
2025-2-23 11:05
QinBeast
感谢你的贡献,论坛因你而更加精彩!
2024-9-28 01:28
t0hka1
为你点赞~
2023-10-25 12:03
hkdong
为你点赞~
2023-8-29 08:43
0xEA
为你点赞~
2023-4-15 14:05
你瞒我瞒
为你点赞~
2023-3-19 19:53
PLEBFE
为你点赞~
2023-1-21 04:17
樊辉
为你点赞~
2022-10-27 12:45
mb_ehlxsliq
为你点赞~
2022-9-29 13:02
rawer886
为你点赞~
2019-10-27 20:44
virjar
为你点赞~
2019-10-26 00:28
最新回复 (20)
雪    币: 878
活跃值: (496)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
确实有不少方法可以检测多开, 从业务安全逻辑上允许用户多开才是最好的防范
2019-10-25 18:44
1
雪    币: 12502
活跃值: (3073)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
rrrfff 确实有不少方法可以检测多开, 从业务安全逻辑上允许用户多开才是最好的防范
公孙策 好难见一次。
2019-10-25 22:18
0
雪    币: 6529
活跃值: (7017)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
4
学习到了,感谢楼主
2019-10-26 11:13
0
雪    币: 8511
活跃值: (5131)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
5
看到 “奔放稳定框架” 我会心一笑
2019-10-26 19:04
0
雪    币: 402
活跃值: (48)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
后记才是本文最大亮点

另外readSymbolicLink可以改linux的readlink读,估计这就是笔者说的ASM大概思路把
2019-10-26 21:00
0
雪    币: 3799
活跃值: (4247)
能力值: ( LV9,RANK:180 )
在线值:
发帖
回帖
粉丝
7
rrrfff 确实有不少方法可以检测多开, 从业务安全逻辑上允许用户多开才是最好的防范
在理,微信中貌似就有适配多开和xposed框架的代码
2019-10-26 21:18
0
雪    币: 18
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
krash 在理,微信中貌似就有适配多开和xposed框架的代码
记得微信不允许多开吧,还是已经改了?
2019-10-27 20:45
0
雪    币: 2
活跃值: (80)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
多开方案都是对抗性质,真实的场景要复杂的多,这个就是玩具啊
2019-11-8 20:14
0
雪    币: 4
活跃值: (592)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
暗街之王 后记才是本文最大亮点 另外readSymbolicLink可以改linux的readlink读,估计这就是笔者说的ASM大概思路把
试过linux的readlink不行啊
2020-4-1 14:48
0
雪    币: 1109
活跃值: (3626)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
11
koftnt 试过linux的readlink不行啊
姿势不对,不要使用 C 函数库的 readlink
2020-4-14 16:18
0
雪    币: 1892
活跃值: (1618)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
12
楼主的ASM代码是指C++中的ASM还是Java中的呢?
2020-5-21 15:08
0
雪    币: 4
活跃值: (592)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
那用什么,命令行?
2020-5-22 19:25
0
雪    币: 180
活跃值: (3851)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
14
其中有一个“A[dataDir的fd] --> B[fd反查真实路径]”这个是什么意思,尤其是怎么得出B的路径的呀,我这边本打算使用C语言来模拟Java来写,但是int fd = open(testPath.c_str(), O_WRONLY | O_CREAT | false ? O_APPEND : O_TRUNC); 返回后的fd为-1;但是Java那边确实是fd不等于-1的,所以特别想问下这个问题,其中的技术原理我应该怎么弄清楚呢,对这块不是很懂。
2021-11-15 19:17
0
雪    币: 4712
活跃值: (3027)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
感谢分享
2021-11-17 09:20
0
雪    币: 180
活跃值: (3851)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
16
我自己用asm实现了一套简单的io函数,但是还是不对,int fd = __openat(AT_FDCWD, testPath.c_str(), O_WRONLY | O_CREAT | false ? O_APPEND : O_TRUNC, 0);返回后的fd还是为-1,真不知道哪里出了问题,有点怀疑是open函数中的flag的值写的有点问题吧,真不知道哪里有问题了,其次最主要的是那个问题我也没有找到答案。
2021-11-17 11:32
0
雪    币: 180
活跃值: (3851)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
17
好吧,我弄出来了,还是很感谢大佬提供的思路的。上面的问题是我的问题,因为Java中的FileOutputStream在new的时候会对不存在的文件进行新建,所以我那个openat的flag是没啥问题,不过我后来改成了O_RDWR | O_CREAT | O_TRUNC。这样就可以写入了,我之所以写成O_WRONLY | O_CREAT | false ? O_APPEND : O_TRUNC完全是看了FileOutputStream的源码,其中它这个flag就是这么写的,所以我直接照抄。然后起最后的mode,FIleOutputStream中mode为0,我也写成0,但是一直是fd为-1,所以我将mode改成0640,这样fd就不为-1了,后面的流程就正常运行了。
2021-11-17 14:38
1
雪    币: 163
活跃值: (1758)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
18
// --- Java ---
import java.io.File;
boolean isDualApp(String dataDir){
    return new File(dataDir + File.separator + "..").canRead();
}
看了源码,最终也是调用acess()函数,为什么检测结果会不一样呢
2022-12-16 16:39
0
雪    币: 1109
活跃值: (3626)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
19
学编程 // --- Java --- import java.io.File; boolean isDualApp(String dataDir){ return new File(dataD ...
证明这个多开容器的 I/O Hook 不够全
2022-12-19 15:38
0
雪    币: 163
活跃值: (1758)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
20
Amun 证明这个多开容器的 I/O Hook 不够全
我的意思是说,通过查看Android源码,canRead的流程最终是要走到access函数,那方式二的效果应该是等同于方式一的。 不应该会出现文中的结果。

测试代码一

// --- C++ ---
#include <unistd.h>
#include <sys/stat.h>
#include <string>
bool isDualApp(std::string dataDir) {
    return 0 == access((dataDir + "/../").c_str(), R_OK);
}
测试代码二
// --- Java ---
import java.io.File;
boolean isDualApp(String dataDir){
    return new File(dataDir + File.separator + "..").canRead();
}
2022-12-21 14:58
0
雪    币: 136
活跃值: (1212)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
mark
2023-8-28 23:08
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册