利用cache特性检测Android模拟器

leonnewton 2016-03-01 15:16:00

Author:leonnewton

0x00 序


目前对Android模拟器的检测,主要是从特定的系统值来进行区分的。例如,getDeviceId()、getLine1Number()这类函数,还有android.os.Build类记录的一系列值等等。但是偶然发现有位老外提出了用cache来区分模拟器和真机的idea,但是这位老外可能当时比较懒,没有具体的细节,写了个简单的PoC后把Evaluation空着了,也没有实验,所以并不知道这个方法是否真的有效。因此,本文就把检测的整个方法从原理到实现完整地展现出来。

0x01 ARM和x86


由于现在大部分的Android手机都是ARM架构的,因此首先看一下ARM架构和x86架构在cache上的区别。两者简明的区别如下图所示。

图1:ARM和x86 cache区别

从图中我们可以看出,在CPU和内存之间,可以存在几级cache,这里是L1和L2。cache的作用是加速,把指令缓存起来,就不用到低速的内存中去取了。x86的cache都是连续的,但是ARM把L1 cache分成了平行的2块,也就是I-Cache和D-Cache。这种将程序指令储存和数据储存分开的存储器结构叫哈佛架构(Harvard architecture),而把程序指令存储器和数据存储器合并在一起的叫冯·诺伊曼结构(von Neumann architecture)。

那么问题就来了,在指令和数据分开存储的结构中,这两个cache不是同步的,因此一个特定地址的数据值在一个cache中更新了,但是在另一个cache就没有更新。比如往数据cache中写了数据,指令cache中是不会写入这个数据的。

而目前Android SDK提供的模拟器是基于QEMU的,QEMU是一个开源的模拟处理器的软件,详细可以看维基QEMU。所以模拟器是没有分开的cache,模拟器只有一个整块的cache。

于是就有了下面利用cache来检测模拟器的思路。

0x02 思路


先看下思路的流程图:

图2:检测思路

左边的是真机上发生的情况,右边是模拟器发生的情况,下面详述一下操作和后果。

第一步:
执行一个地址上的指令,假设就是$address这个地址。那么在真机上,指令会写到I-Cache上,模拟器直接写到cache上(因为模拟器就一个整块的cache)。

第二步:
$address写入一个新指令。注意,这就有区别了,真机上的新指令会写入D-Cache,而在模拟器直接写到cache上。

第三步:
执行$address的指令。那么此时,在真机上,会从I-Cache读指令,也就是会执行第一步的指令。模拟器直接从cache上读指令,会执行第二步的新指令。

当然有可能发生在真机上的指令cache被洗掉了,但是实验下来可能性还是比较小的。

0x03 show me the code


首先是设计一段代码,会向一个特定的地址重新写一个指令。然后由于要重新回到原来的地址再执行一遍,因此可以用一个循环来实现。代码如下:

__asm __volatile (
1 "stmfd sp!,{r4-r8,lr}\n"
2 "mov r6,#0\n"  用来统计循环次数debug用的
3 "mov r7,#0\n"  为r7赋初值
4 "mov r8,pc\n"  47行用来获得覆盖$address新指令的地址
5 "mov r4,#0\n"  为r4赋初值
6 "add r7,#1\n"  用来覆盖$address的新指令
7 "ldr r5,[r8]\n" 
8 "code:\n"
9 "add r4,#1\n"  这就是$address是对r4加1
10 "mov r8,pc\n"  10,11,12行的作用就是把第6行的指令写到第9行
11 "sub r8,#12\n"
12 "str r5,[r8]\n"
13 "add r6,#1\n"   r6用来计数
14 "cmp r4,#10\n"  控制循环次数
15 "bge out\n"
16 "cmp r7,#10\n"   控制循环次数
17 "bge out\n"
18 "b code\n"      10次内的循环调回去
19 "out:\n"
20 "mov r0,r4\n"    把r4的值作为返回值
21 "ldmfd sp!,{r4-r8,pc}\n"
);

注释已经解释得比较清晰了。也就是说,r4如果是10,那么就是执行的是旧指令,是在真机上。如果r4等于1,那就是执行了旧指令,是在模拟器上。

这里会遇到一个问题,就是我们是没有写代码段的权限的,解决方案是mmap一段可写的,把编译好的机器码复制进去,再跳过去执行。

void (*call)(void);
#define PROT PROT_EXEC|PROT_WRITE|PROT_READ
#define FLAGS MAP_ANONYMOUS| MAP_FIXED |MAP_SHARED
char code[]=
"\xF0\x41\x2D\xE9\x00\x60\xA0\xE3\x00\x70\xA0\xE3\x0F\x80\xA0\xE1"
"\x00\x40\xA0\xE3\x01\x70\x87\xE2\x00\x50\x98\xE5\x01\x40\x84\xE2"
"\x0F\x80\xA0\xE1\x0C\x80\x48\xE2\x00\x50\x88\xE5\x01\x60\x86\xE2"
"\x0A\x00\x54\xE3\x02\x00\x00\xAA\x0A\x00\x57\xE3\x00\x00\x00\xAA"
"\xF5\xFF\xFF\xEA\x04\x00\xA0\xE1\xF0\x81\xBD\xE8";
void *exec = mmap((void*)0x10000000,(size_t)4096 ,PROT ,FLAGS,-1,(off_t)0);
memcpy(exec ,code,sizeof(code)+1);
call=(void*)0x10000000;
call();

申请了一段内存,然后把汇编代码的机器码复制过去,接着跳到这块内存执行。然后我们在后面取r4的值即可。

__asm __volatile (
"mov %0,r0\n"
:"=r"(a)
:
:
);

把r0,也就是r4的值放到a变量中。然后根据a的值返回不同的值就可以了。方便在应用里判断结果。

0x04 调试


调试的方法可以见郑博士的文章安卓动态调试七种武器之孔雀翎 – Ida Pro

整个调试的过程是,把上一节的代码编译成一个so共享库,返回值是r0也就是r4的值(a变量),然后在应用中根据返回值来判断在什么环境中运行。

在进入10000000前下断点,然后F7进去。

进入以后,在mov r0,r4的时候下断,F9执行,这时候看到r4的值是10,这是在真机上测试的结果。可以看到原先add r4,#1 已经变成了add r7,#1,但是实际执行的还是add r4,#1。

在模拟器执行的结果如下,可以看到r4的值是1,r7是10,所以执行的是新指令,是在模拟器上:

0x05 测试


不知道在其他机器上是否可行,大家可以从https://github.com/leonnewton/cache_test下载进行测试。

评论

C

chen 2016-03-01 17:57:19

hui

路人甲 2016-03-01 22:48:52

Exciting!

X

xsser 2016-03-02 10:30:09

Hacking!

A

angelwhu 2016-03-02 10:35:37

点赞~~

J

JGHOOluwa 2016-03-02 11:46:02

666~

Mark 2016-03-03 12:16:01

应用场景呢?

L

leonnewton 2016-03-03 12:30:55

@Mark 恶意代码检测到是模拟器的时候可以不释放有效载荷。

路人甲 2016-03-03 18:00:34

git上要是能给出代码那就太完美了

路人甲 2016-03-03 22:12:17

6.0不行,软件直接崩掉

L

leonnewton 2016-03-03 22:23:51

@u4sj 好的,我马上调试下。

F

Finder 2016-03-04 18:47:55

只有arm..

路人甲 2016-04-28 16:04:07

能不能发一份代码到我邮箱。下载了apk 报错 [email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */

路人甲 2016-05-03 16:27:16

能不能发一份代码到我邮箱, [email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */,谢谢。

路人甲 2016-05-09 14:56:21

博主,你试验过么?

路人甲 2016-05-18 18:24:22

这个好像只能检测运行cpu是否是x86,对于cpu为Intel Atom cpu的Android手机是有误报的,如asus,dell和acer系列手机cpu架构就是x86的

路人甲 2016-05-27 17:16:12

@leonnewton 6.0crash的概率还挺高的,memory violation

L

leonnewton 2016-06-07 10:58:50

@gavin crash的可能原因是我直接指定了地址0x10000000,可以根据实际分配地址,当时是为了调试方便直接写了一个。

路人甲 2016-06-07 20:29:49

这个在三星手机上会crash,错误信息如下:
libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x10000000 in tid 16990 (AsyncTask #1)
请教一下,这个该如何解决?多谢

L

leonnewton 2016-06-10 11:31:04

@bob223946 把0x10000000这个地址换成其他可以用的,或者直接是申请可用的。

路人甲 2016-06-15 10:53:52

这边试了很多其他方法,很多模拟器很难打到,这个cache还是很精确,
能不能发份代码,[email protected]/* <![CDATA[ */!function(t,e,r,n,c,a,p){try{t=document.currentScript||function(){for(t=document.getElementsByTagName('script'),e=t.length;e--;)if(t[e].getAttribute('data-cfhash'))return t[e]}();if(t&&(c=t.previousSibling)){p=t.parentNode;if(a=c.getAttribute('data-cfemail')){for(e='',r='0x'+a.substr(0,2)|0,n=2;a.length-n;n+=2)e+='%'+('0'+('0x'+a.substr(n,2)^r).toString(16)).slice(-2);p.replaceChild(document.createTextNode(decodeURIComponent(e)),c)}p.removeChild(t)}}catch(u){}}()/* ]]> */,万分感谢啊

L

leonnewton

风乍起,吹皱一池春水。

twitter weibo github wechat

随机分类

二进制安全 文章:77 篇
PHP安全 文章:45 篇
无线安全 文章:27 篇
运维安全 文章:62 篇
其他 文章:95 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录