Android模拟器检测及对抗方法
参考:http://zeng9t.com/tech/2019/04/30/Android%E6%A8%A1%E6%8B%9F%E5%99%A8%E6%A3%80%E6%B5%8B%E5%8F%8A%E5%AF%B9%E6%8A%97%E6%96%B9%E6%B3%95.html
前言
因为做项目的原因,对目前检测模拟器环境的方法大概有了一个了解,有业界方法也有学术界的方法。如何进行对抗的方法,在后续进行说明。 做项目就是边学边做的过程中不断学习,知识不可能学完,并且计算机相关的知识和技术变化迭代很快,需要用到的时候再学再扩展,保持一颗学习的心及学习方法很重要。
检测模拟器环境的不同用途
关于emulator环境的检测,业界的应用比如阿里等公司,防止一些利益集团进行淘宝恶意刷单、支付宝注册得红包等褥羊毛操作;还有比如手游防止玩家使用模拟器进行操作造成了游戏的不公平性。而恶意应用会利用模拟器检测技术来防止在emulator环境下被动态分析,因此会检测emulator环境,然后在emulator环境下触发不同的代码路径(通常是非恶意的代码逻辑)。
检测核心思想
利用emulator和真机的区别,当然这种区别是可检测的,相对易于实现的,根据具体需求来衡量使用什么样的方法是足够的,并且可扩展、可维护、对程序运行效率影响小。
常用的实用方法
TelephonyManager类
- getLine1Number
- getDeviceId
- getSubscriberId
- getVoiceMailNumber
- getSimSerialNumber
Build信息
- BRAND == generic
- DEVICE == generic
- HARDWARE == goldfish
- PRODUCT == sdk
- HOST == android-test
- TAGS == test-keys
特征文件
- /dev/socket/qemud
- /dev/qemu_pipe
- /system/lib/libc_malloc_debug_qemu.so
- /sys/qemu_trace
- /system/bin/qemu-prop
系统属性
- ro.hardware == goldfish
- ro.product.device == generic
- ro.product.model == sdk
- ro.product.name == sdk
基于差异化信息
- /proc/cpuinfo真机CPU一般都是基于ARM,模拟器一般为Intel或AMD
- 模拟器通话记录、联系人、短信等通常为空
- 检查是否存在Dev Tools等模拟器上特有的应用程序
基于硬件数据
- 可以多次检测单个传感器数据,但部分传感器模拟器也可以模拟实现。检测传感器数量,模拟器传感器数量一般无法超过10,而一般手机传感器数量大于20;多次对传感器数据取值观察是否变化等方法来判断。常见传感器有:陀螺仪、加速度计、[心率传感器]、[光线传感器]、[压力传感器]、[计步器]、重力传感器、旋转矢量传感器等,其中括号中的传感器数据一般模拟器较难进行模拟
- 检查电池的电压、电量、温度等是否实时变化
- 触摸面积:真机变化且不为0,模拟器为0
基于应用层行为数据
- 例如安装应用数量,是否具备常见应用,是否有联系人等。对设备的行为模式进行统计分析,作为风险设备画像的参考维度。
基于cache行为
- ARM采用的哈弗架构将指令存储跟数据存储分开,ARM的一级缓存分为I-Cache(指令缓存)与D-Cache(数据缓存),而Simpled X86只有一块缓存,而模拟器可以看做是Simpled-x86架构。如果将一段代码可执行代码动态映射到内存,在执行的时候,Simpled-X86架构上动态修改这部分代码后,指令cache会被同步修改,而ARM修改的却是D-Cache中的内容,此时I-Cache中的指令并不一定被更新,因此程序就会在ARM与Simpled-x86上有不同的表现,根据计算结果便可以知道究竟是x86还是在ARM平台上运行。
- 无论是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 emulator_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;
}
防止在真机上出现崩溃,最好单独开一个进程服务,利用Binder实现模拟器鉴别的查询。再结合其他检测方法做综合度量。
基于指令执行行为
- 为了效率上的考虑,qemu在翻译执行ARM指令时并没有实时更新模拟的pc寄存器值,只会在一段代码翻译执行完之后再更新,而真机中pc寄存器是一直在更新的。可以设计CPU任务调度程序以检测模拟器。
对抗
针对大部分Java层检测方法和部分native层检测方法都可以被hook掉,可以考虑在native code中进行检测,并使用自定义API替换相应的系统API,防止被hook,面对一些特殊处理后的模拟器具有相对较好的检测效果。
说明
检测方法有很多,例如还有基于mac信息和蓝牙信息等,可以利用多种检测手段进行综合分析,但也要考虑到具体需求,不要过度检测以免影响应用运行效率。
检测示例
public static boolean isEmulatorAbsoluly() {
if (Build.PRODUCT.contains("sdk") ||
Build.PRODUCT.contains("sdk_x86") ||
Build.PRODUCT.contains("sdk_google") ||
Build.PRODUCT.contains("Andy") ||
Build.PRODUCT.contains("Droid4X") ||
Build.PRODUCT.contains("nox") ||
Build.PRODUCT.contains("vbox86p")) {
return true;
}
if (Build.MANUFACTURER.equals("Genymotion") ||
Build.MANUFACTURER.contains("Andy") ||
Build.MANUFACTURER.contains("nox") ||
Build.MANUFACTURER.contains("TiantianVM")) {
return true;
}
if (Build.BRAND.contains("Andy")) {
return true;
}
if (Build.DEVICE.contains("Andy") ||
Build.DEVICE.contains("Droid4X") ||
Build.DEVICE.contains("nox") ||
Build.DEVICE.contains("vbox86p")) {
return true;
}
if (Build.MODEL.contains("Emulator") ||
Build.MODEL.equals("google_sdk") ||
Build.MODEL.contains("Droid4X") ||
Build.MODEL.contains("TiantianVM") ||
Build.MODEL.contains("Andy") ||
Build.MODEL.equals("Android SDK built for x86_64") ||
Build.MODEL.equals("Android SDK built for x86")) {
return true;
}
if (Build.HARDWARE.equals("vbox86") ||
Build.HARDWARE.contains("nox") ||
Build.HARDWARE.contains("ttVM_x86")) {
return true;
}
if (Build.FINGERPRINT.contains("generic/sdk/generic") ||
Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86") ||
Build.FINGERPRINT.contains("Andy") ||
Build.FINGERPRINT.contains("ttVM_Hdragon") ||
Build.FINGERPRINT.contains("generic/google_sdk/generic") ||
Build.FINGERPRINT.contains("vbox86p") ||
Build.FINGERPRINT.contains("generic/vbox86p/vbox86p")) {
return true;
}
return false;
}
反检测
既然有检测方法,那么最常见的反检测方法可以自然地想到是针对这些检测方法,做hook,因为大部分检测方法在调用链上最终都是需要通过相应的API来获取相关的数据。有就Java层的hook也有native层的hook,Android上常见的Hook框架有Xposed和Frida等,Xposed对x86的兼容性较差,Frida的跨平台兼容性较好。部分字段是系统镜像自带属性,因此可通过修改AOSP源码自编译系统镜像再装入emulator。
Some Paper Resources
Evading Android Runtime Analysis via Sandbox Detection
Rethinking anti-emulation techniques for large-scale software deployment