导语:本篇文章会介绍如何对root权限和调试环境进行检测,以及在真实设备上验证该方案的实际检测结果。

对root权限进行检测

本节会介绍对root权限进行检测的技术思路,这些思路我们都在上文中介绍过。就是基于这些方法,研究人员制作了一个具有root权限属性的检测表,并设计了几个函数(如上所示)来检测它们。在检测表中,研究人员将上文所讲过的第二种检测方法(检测已安装文件)细化为两项检测:检测su二进制文件和检测来自busybox的命令。研究人员并没有将上文所讲过的第七中检测方法(使用shell命令检测root权限)包含在检测表中,因为该方法可以很容易被其他方法替代。研究人员使用的是开源软件实现的这些检测功能,检测表和函数的具体运行过程如下所示:

· detectTestKeys()会检测自定义镜像闪烁,它会reads /system/build.prop并检测ro.build.tags的值是否为“release-key”。如果没有,则假定设备具有root权限;

· checkForSystemProperties()检测系统属性的值,它会调用Runtime().exec(“getprop”)获取系统属性,然后检测ro.secure和ro.debuggable的值;

· checkForBinary ()会尝试查找安装的su二进制文件,它调用exist()

· 检测su二进制文件是否安装在类似/system/(x)bin, /data/local/(x)bin等中;

· checkForBusybox()会检测busybox提供的其他命令存在与否,它通过调用Runtime.exec()来执行find,tail和lsof等命令;

· checkForFilesystem()会检测文件系统属性,它会调用mount命令来检测任何只读文件系统是否具有“write”属性;

· detectRootManagementApps()会调用getPackageInfo()来获取已安装应用程序的列表,并将其与具有rooti权限相关的应用程序列表进行比较;

· detectRootProcess()会调用Runtime.exec(“ps”)获取当前正在运行的进程的列表,并使用root权限检测进程。如果这样的过程不是Zygote进程的子过程,则就假设设备是含有root权限的;

3.jpg

使用NetBeans动态调试Android应用程序

对调试环境进行检测

市场上的Android应用程序通常是在发布模式下构建的,并且在发布模式下会删除调试标志。要启用动态调试,攻击者就需要使用apktool等工具激活该标志。该标志通过将android:debuggable=true插入到AndroidManifest.xml中来激活。

Android应用程序的java级调试通常使用NetBeans进行,调试环境如图3所示。研究人员可以通过检测标志的值来检测调试环境。通过使用Android服务包管理器,存根DEX将获得ApplicationInfo类。这个类的ApplicationInfo.flags Field 字段包含当前运行的应用程序的各种标志值。存根DEX会检测该Field 字段是否包含来自android:debuggable的FLAG DEBUGGABLE。如果是,则终止该应用程序(假设攻击者正在调试),这个过程是使用isDebuggable()方法实现的。

对基于调用栈的那些试图绕过安全检测攻击的检测

调用堆栈是用于执行Java应用程序方法的内存空间。调用时,调用堆栈将为该执行方法分配其中的一些内存。内存通常存储本地变量、参数变量、临时操作数据等。通过对调用堆栈的研究,研究人员可以识别出调用者和被调用者方法并获得一系列方法调用。由于Android应用程序是一种Java应用程序,我们可以通过比较方法名来获取Android应用程序的调用方法信息和挂钩(那些试图绕过安全检测的攻击) 方法。

· 检测表使用的存根DEX方法如下所示:

· 调试的标志用isDebuggable()检测;

存根DEX会在研究人员希望预防的那些试图绕过安全检测的攻击的函数中生成异常,他们会通过调用getStackTrace()方法获取调用堆栈的内容。getStackTrace()会返回StackTraceElement对象数组,其中包含每个被调用方法的类和方法名称。逃脱方法通常要挂钩系统调用或库函数,并修改它们的参数或返回值来更改执行流程。为了在Android上挂钩库函数,许多攻击者使用Xposed。Xposed是一个众所周知的框架,它可以改变Android系统或应用程序的行为。它会扩展了Zygote来加载一个用于挂钩的库,而Zygote加载的每个应用程序都含有这些挂钩库。这些库会提供了一个findAndHookMethod()方法,它会挂钩指定的方法并注册回调方法。每次调用挂钩方法时,都会调用handleHookedMethod()。该函数不但会调用已注册的回调方法,还可以更改调用的参数、调用其他方法并修改结果。

4.jpg

检测与root相关的应用程序时,所调用的存根DEX的堆栈图

图4显示了存根DEX调用detectRootManagementApps()来检测与root相关的应用程序时的调用堆栈。当应用程序启动时,会首先调用onCreate()。然后调用doRootCheck(),它会创建一个包含root权限的检测对象。该对象会先调用isRooted(),然后调用detectRootManagementApps()。该方法会获取一个与root权限相关的应用程序列表,并调用detectResult(),以将其与设备中安装的应用程序进行比较。注意,call stack()是一个生成异常对象并检测其中是否包含调用堆栈内容的方法。

5.jpg

当detectRootManagementApp()被挂钩时,存根DEX的调用堆栈

此时攻击者可能会挂钩detectRootManagementApps(),并修改包含root权限的应用程序列表。从onCreate()到detectRootManagementApps()的方法调用与图4中含有相同。在图5中,存根DEX中并没有使用handleHookedMethod()和invokeOriginalMethodNative()两种方法,因为这些方法属于Xposed,可以修改与root相关的应用程序列表。为了防御这种那些试图绕过安全检测的攻击,存根DEX会在根或调试检测方法的过程中调查它的调用堆栈。通过将调用堆栈中的方法与存根DEX中使用的方法列表进行比较,存根DEX会检测挂钩或那些试图绕过安全检测的攻击,并终止应用程序。

6.jpg

自定义镜像闪烁的堆栈跟踪和检测结果

检测结果的验证

研究人员成功将该方案应用于Android应用程序,验证程序是《部落冲突》(clash Of Clans),是从Google Play下载的。研究人员向其中添加一个存根DEX(称为classes.dex),将原来的classes.dex文件移动到目录资产,并重新打包APK。此时,添加的存根DEX会对原始类执行root权限或调试环境检测、逃脱(挂钩)检测和动态加载。

验证过程是在真正的智能手机Nexus 4((Android 4.4 Kitkat, Linux kernel 3.5)上进行的,另外研究人员还为每个检测列表设置一个root环境,并启动重新打包的验证应用程序来检测是否检测到对应的root环境。最后,研究人员还设置了一个调试环境,如图3所示,这是为了验证是否检测到调试环境。接下来,他们会在验证系统中构建并安装Xposed,以避免root权限或调试检测。通过比较存根DEX的调用栈,就可以证明此预防方案是否可以检测到那些试图绕过安全检测的攻击。

对预防那些使用Xposed试图绕过安全检测的验证

首先,研究人员会检测在无效Xposed的root权限设备上的存根DEX的调用堆栈,该设备一个通过闪烁Cyanogenmod(flashing Cyanogenmod)自定义镜像的root权限设备。图6显示了存根DEX的堆栈跟踪,以及启动重新打包的验证应用程序时的检测结果。存根DEX的每个堆栈跟踪都包括方法调用onCreate()、doRootCheck()和isRooted()。当调用存根DEX中的onCreate()时, doRootCheck()就会被调用,同时doRootCheck()会创建root权限检测对象。

该对象会包含要检测每个具有root权限或调试属性的方法(上文的检测方法中,已经详细列出来了)。这些方法由具有root权限检测对象的isRooted()方法一个接一个地调用。例如,在图6中,isRooted()调用detectTestKeys()来检测ro.build.tags的值。在图6中,detectTestKeys()检测自定义镜像闪烁并输出日志““[-] Detect test-keys(检测验证密钥)”。它还会弹出一条Toast消息并终止该应用程序。

接下来,我们会检测激活Xposed后,存根DEX的调用堆栈。图7显示了使用Xposed的那些试图绕过安全检测的攻击下的调用堆栈。Xposed的handleHookedMethod()和invokeOriginal MethodNative()方法都会出现在调用堆栈中。由于这些方法会修改detectTestKeys()的返回值,因此,isRooted()无法检测自定义镜像闪烁,其他具有root权限属性检测方法的返回值,也会进行了类似的修改。

在调试检测验证中,研究人员在验证应用的AndroidManifest.xml中设置了“android:debuggable=true”标志并重新打包。通过这种方式设置的调试环境,如图3所示,然后启动应用程序即可。此时存根DEX会创建根检测对象,并调用isDebuggable()方法。该方法会检测android:debuggable 标志的值,并输出日志“[-]detection debug mode!!”

7.jpg

逃脱root权限检测时的堆栈跟踪

8.jpg

检测到调试环境时的堆栈跟踪

图8显示了检测到调试环境时的调用堆栈,图9显示了使用Xposed逃脱调试检测时的调用堆栈。与检测那些逃脱root权限检测的过程类似,当调用isDebuggable()时,将调用Xposed的挂钩方法,此时isDebuggable()的返回值被修改,安全检测失败。图10是调试验证应用程序的NetBeans的屏幕截图。

对预防那些基于调用栈逃脱的结果验证

研究人员发现,当攻击者使用Xposed等工具执行那些试图绕过安全检测的攻击时,现有的root权限和调试环境检测技术无法正常工作。在这一部分,研究人员会验证所提出的解决方案是否能检测出那些试图绕过安全检测的攻击。

9.jpg

逃脱调试环境检测时的堆栈跟踪

10.jpg

使用NetBeans调试验证应用程序

11.jpg

在root权限环境中检测进行逃脱攻击

图11可以明显的显示出,新的预防方案是如何保护验证应用程序在root环境中免受那些试图绕过安全检测攻击的。

对于那些试图绕过安全检测的攻击,现有的root属性检测方法,比如detectTestKeys())是不会检测到那些自定义镜像闪烁的,请注意“[+] Device is not custom rom([+]设备不是自定义rom)”日志。但是,Xposed (invokeOriginalMethodNative()和handleHookedMethod())的挂钩方法却出现了。

在调用堆栈中,新的预防方案可以成功检测出基于调用堆栈的逃脱攻击,因为它会成功地检测Xposed的挂钩方法,注意此时是“[-] But this function is hacked!!(这个函数被黑了)”的日志。图12显示了所检测到那些绕过调试环境检测的攻击,你可以看到isDebuggable()进行了挂钩。调试模式检测方法未能检测到调试标志,但新的方案可以发现这种挂钩。

12.jpg

对调试环境中那些试图绕过安全检测攻击的检测

检测时间估计

这个新的检测方案中,存根DEX的大小为21.7KB。至于检测所花费的时间,会根据实际检测的root权限或调试环境来定,其原理就是确定是否有被挂钩的方法以及相应的原始DEX文件是否被加载。

总结

由于Android应用程序是用Java编程语言编写的,并且Java字节码可以使用逆向工程工具轻松地反编译,因此它们很容易受到静态和动态逆向工程攻击。为了保护Android应用程序免受静态逆向工程的影响,研究人员已经采用了混淆、动态代码加载、打包等预防措施。然而,它们却无法预防动态逆向工程攻击。近些年,就已经出现了许多基于真实设备的动态逆向工程攻击,可以转储应用程序代码并对其进行逆向工程。由于这种动态攻击在攻击时,需要在真实设备上获取root权限。

所以在本文中,研究人员提出了一种Android应用程序保护方案。该方案就是实现对root权限或调试环境的检测。这些检测技术可以检测那些使用Xposed框架逃避检测的攻击,如本文所述,该方案还实现了基于调用堆栈的逃脱检测技术,该技术可以检测由Xposed框架启用的挂钩,该方案的优点是它使用的是动态代码加载技术,以最终应用于任何编译(打包)的Android应用程序中。

源链接

Hacking more

...