基于Cache的Android模拟器检测
本文主要解决了ARM64位指令的兼容性问题,并通过进程间通信杜绝了崩溃现象,让这部分的检测代码更具有可操作性。。
ARM和X86
目前,绝大部分手机都是基于ARM架构,其他CPU架构给忽略不计,模拟器全部运行在PC的X86架构上。因此,可以利用ARM与X86的区别来判断是否为真机。
从上图我们可以看出,在CPU和内存之间,可以存在几级Cache,这里是L1和L2。Cache的作用是加速,把特定地址的数据值缓存起来,这样就不用到低速的内存中去取了。其中,ARM采用的是将指令存储与数据存储分开的哈佛架构,L1 Cache(一级缓存)被分成了平行的两块,即I-Cache(指令缓存)和D-Cache(数据缓存),而X86采用的是将指令存储和数据存储合并在一起的冯•诺伊曼结构,L1 Cache是连续的一块缓存。
因此,如果我们通过读写地址指令的方式对一段可执行代码进行动态修改,那么在执行的时候,X86架构上的指令缓存会被同步修改,而对ARM架构而言,这种数据读写操作修改的只是D-Cahce中的内容,此时I-Cache中的指令并不会被更新。
设计思路
先看下思路的流程图:
左边的是真机上发生的情况,右边是模拟器发生的情况,下面详述一下操作过程。
- 先执行一个地址上的指令,假设就是address这个地址。那么对于真机,指令将会写到I-Cache,而模拟器则会直接写到一整块Cache上;
- 向address写入一个新指令。注意,这就有区别了,真机上的新指令会写入D-Cache,而在模拟器直接写到Cache;
- 执行address的指令。此时在真机上,会从I-Cache读指令,也就是会执行第一步的指令。模拟器直接从Cache上读指令,会执行第二步的新指令。
实际操作中,我们发现存在真机上的指令Cache被洗掉的情况,但总的来说这种可能性还是比较低的,可以通过对市面上的大量机型做多次重复实验,并统计它的大盘分布..
具体实现
测试代码
ARM32
以下实现代码是测试代码的核心,主要就是将第8行地址的指令add r4, r4, #1,在运行中动态替换为第5行的指令add r6, r6, #1,这里的目标是ARM-V7架构的,要注意它采用的是三级流水,PC寄存器的值等于当前程序执行位置加8,代码如下:
1 | #!cpp |
ARM64
考虑到在64位的真机上可能会存在兼容性问题,需要针对arm64-v8a架构重新设计一段代码,原理同上。另外,由于arm64指令集中没有PC寄存器,这里选择用ADR指令做为替代方案。。
1 | #!cpp |
申请权限
这里会遇到个问题,就是我们是没有写代码段的权限的,所以需要将上面的汇编代码翻译成相应的机器码,再申请一块内存,将可执行代码段拷贝过去并执行。值得注意的是,如果用mmap映射会有bug,在真机上只能执行一次,第二次崩溃,可以通过如下方式解决:
1 |
|
验证方式
如果返回值等于2,说明执行的是旧指令,是arm架构;如果返回值等于1,说明执行的是新指令,是x86架构。最后,由于真机上存在I-Cache被洗掉的情况,也可能返回1,故需要通过多次循环执行对返回的结果进行统计,越靠近1~1000这个范围值的左侧,越可能是真机,反之应是模拟器。
备注
最后,为了防止在真机上出现崩溃,最好还是单独开一个进程服务,通过进程间通信实现模拟器鉴别的查询。。