Android模拟器常常被用来刷单,如何准确的识别模拟器成为App开发中的一个重要模块,目前也有专门的公司提供相应的SDK供开发者识别模拟器。 目前流行的Android模拟器大概分为两种,一种是基于Qemu,另一类是基于Genymotion(VirtualBox类),网上现在流行用一些模拟器特征进行鉴别,比如:
不过里面的很多手段都能通过改写ROM或者Xposed作假,让判断的性能打折扣。其实,现在绝大部分手机都是基于ARM架构,其他CPU架构给忽略不计,模拟器全部运行在PC上,因此,只需要判断是运行的设备否是ARM架构即可。
ARM与Simpled X86在架构上有很大区别,ARM采用的哈弗架构将指令存储跟数据存储分开,与之对应的,ARM的一级缓存分为I-Cache(指令缓存)与D-Cahce(数据缓存),而Simpled X86只有一块缓存,而模拟器采用的可以看做是Simpled-x86架构,如果我们将一段代码可执行代码动态映射到内存,在执行的时候,Simpled-X86架构上动态修改这部分代码后,指令缓存会被同步修改,而ARM修改的却是D-Cahce中的内容,此时I-Cache中的指令并不一定被更新,这样,程序就会在ARM与Simpled-x86上有不同的表现,根据计算结果便可以知道究竟是还在ARM平台上运行,为什么说模拟器采用的是Simpled-x86架构,拿QEMU来说,它采用了一些手段,主动保证了Self-Modifying Code的同步性,看QEMU对于Self-Modifying Code的处理:
On RISC targets, correctly written software uses memory barriers and cache flushes, so some of the protection above would not be necessary. However, QEMU still requires that the generated code always matches the target instructions in memory in order to handleexceptions correctly.
无论是x86还是ARM,只要是静态编译的程序,都没有修改代码段的权限,所以,首先需要将上面的汇编代码翻译成可执行文件,再需要申请一块内存,将可执行代码段映射过去,执行。
以下实现代码是测试代码的核心,主要就是将地址e2844001的指令add r4, r4, #1,在运行中动态替换为e2877001的指令add r7, r7, #1,这里目标是ARM-V7架构的,要注意它采用的是三级流水,PC值=当前程序执行位置+8。通过arm交叉编译链编译出的可执行代码如下:
8410: e92d41f0 push {r4, r5, r6, r7, r8, lr}
8414: e3a07000 mov r7, #0
8418: e1a0800f mov r8, pc // 本平台针对ARM7,三级流水 PC值=当前程序执行位置+8
841c: e3a04000 mov r4, #0
8420: e2877001 add r7, r7, #1
....
842c: e1a0800f mov r8, pc
8430: e248800c sub r8, r8, #12 // PC值=当前程序执行位置+8
8434: e5885000 str r5, [r8]
8438: e354000a cmp r4, #10
843c: aa000002 bge 844c <out>
.....
如果是在ARM上运行,e2844001处指令无法被覆盖,最终执行的是add r4,#1 ,而在x86平台上,执行的是add r7,#1 ,代码执行完毕, r0的值在模拟器上是1,而在真机上是10。之后,将上述可执行代码通过mmap,映射到内存并执行即可,具体做法如下,将可执行的二进制代码直接拷贝可执行代码区,去执行
void (*asmcheck)(void);
int detect() {
//可执行二进制代码
char code[] =
"\xF0\x41\x2D\xE9"
"\x00\x70\xA0\xE3"
"\x0F\x80\xA0\xE1"
"\x00\x40\xA0\xE3"
"\x01\x70\x87\xE2"
"\x00\x50\x98\xE5"
"\x01\x40\x84\xE2"
....
// 映射一块可执行内存 PROT_EXEC
void *exec = mmap(NULL, (size_t) getpagesize(), PROT_EXEC|PROT_WRITE|PROT_READ, MAP_ANONYMOUS | MAP_SHARED, -1, (off_t) 0);
memcpy(exec, code, sizeof(code) + 1);
//强制赋值到函数
asmcheck = (void *) exec;
//执行函数
asmcheck();
__asm __volatile (
"mov %0,r0 \n"
:"=r"(a)
);
munmap(exec, getpagesize());
return a;
}
经验证, 无论是Android自带的模拟器,还是夜神模拟器,或者Genymotion造假的模拟器,都能准确识别。在32位真机上完美运行,但是在64位的真机上可能会存在兼容性问题,可能跟arm64-v8a的指令集不同有关系,也希望人能指点。为了防止在真机上出现崩溃,最好还是单独开一个进程服务,利用Binder实现模拟器鉴别的查询。
另外,对于Qemu的模拟器还有一种任务调度的检测方法,但是实验过程中发现不太稳定,并且仅限Qemu,不做参考,不过这里给出原文链接:
仅供参考,欢迎指正
作者:看书的小蜗牛
原文链接 Android模拟器识别技术