通用软件保护手段
- 源码层级保护:在编码过程中实现的攻防手段
- ① 增加分析复杂度:多线程、虚函数、模板、静态STL库、回调等
- ② 常规攻防能力:AntiHook/AntiDebug/AntiInject/RootDetect/EmulatorDetect/...
- ⑤ 二进制层级保护:在生成软件后,对二进制进行处理进行加固的手段
本文着重介绍①和③,其他方式网上资料较多这里不再赘述。对于②提供如下参考资料:
https://github.com/obfuscator-llvm/obfuscator
Android常规攻防能力表:
调试检测(Debug) |
反调试(AntiDebug) |
注入检测(Inject) |
反注入(AntiInject) |
Hook检测(Hook)) |
反Hook(AntiHook) |
Root检测(RootDetect) |
模拟器检测(EmulatorDetect) |
进程名检测 默认调试端口检测 进程调试状态检测 内存断点检测 内存读写检测 <font color=#FF0000 bgcolor=orange>调试协议测试</font> 进程文件二进制匹配 |
ptrace保护 <font color=#FF0000 bgcolor=orange>JDWP握手信号拦截</font> <font color=#FF0000 bgcolor=orange>JDWP调试模型破坏</font> Jni层反调试 |
常见注入工具检测 环境变量检测 加载模块检测 注入端口检测 |
检测加载模块 |
常见Hook框架检测 系统文件修改检测 进程模块检测 <font color=#FF0000 bgcolor=orange>Java native hook检测</font>
|
内存模块恢复 |
常见su文件检测 系统目录权限检测 Root工具检测 |
系统属性检测 特殊文件检测 手机号、硬件ID检测 …… |
注:加红为我们自己新增的高级功能
C++模板元常量字符串混淆
模板元是C++ 11引入的新概念,用于将一些简单函数执行流程(分支/循环)放在编译期完成,以提高运行期性能甚至完成一些特殊功能。在展示一个完整的模板元编程实现字符串加密函数前,这里先提供一些准备知识:
- C++ 11提供constexpr关键字用于定义编译期常量
- inline关键字是向编译器请求内联;force_inline关键字是强制编译器内联
-
TIME用于在编译期获取时间
line01 #define force_inline __attribute__((always_inline))
line02 #define naked __attribute__ ((naked))
line03
line04 #ifndef vxSEED
line05 // If you don't specify the seed for algorithms, the time when compilation
line06 // started will be used, seed actually changes the results of algorithms...
line07 #define vxSEED ((__TIME__[7] - '0') * 1 + (__TIME__[6] - '0') * 10 + \
line08 (__TIME__[4] - '0') * 60 + (__TIME__[3] - '0') * 600 + \
line09 (__TIME__[1] - '0') * 3600 + (__TIME__[0] - '0') * 36000)
line0A #endif
line0B // The constantify template is used to make sure that the result of constexpr
line0C // function will be computed at compile-time instead of run-time
line0D template<uint32_t Const>
line0E struct vxConstantify {
line0F enum {
line10 VALUE = Const
line11 };
line12 };
line13
line14 // Compile-time mod of a linear congruential pseudorandom number generator,
line15 // the actual algorithm was taken from "Numerical Recipes" book
line16 constexpr uint32_t vxRandom(uint32_t Id) {
line17 return (1013904223 + 1664525 * ((Id > 0) ? (vxRandom(Id - 1)) : (vxSEED))) &
line18 0xFFFFFFFF;
line19 }
line1A
line1B // Compile-time random macros, can be used to randomize execution
line1C // path for separate builds, or compile-time trash code generation
line1D #define vxRANDOM(Min, Max) (Min + (vxRAND() % (Max - Min + 1)))
line1E #define vxRAND() (vxConstantify<vxRandom(__COUNTER__ + 1)>::VALUE)
line1F
line20 // Compile-time recursive mod of string hashing algorithm,
line21 // the actual algorithm was taken from Qt library (this
line22 // function isn't case sensitive due to vxTolower)
line23 constexpr char vxTolower(char Ch) {
line24 return (Ch >= 'A' && Ch <= 'Z') ? (Ch - 'A' + 'a') : (Ch);
line25 }
line26
line27 constexpr uint32_t vxHashPart3(char Ch, uint32_t Hash) {
line28 return ((Hash << 4) + vxTolower(Ch));
line29 }
line2A
line2B constexpr uint32_t vxHashPart2(char Ch, uint32_t Hash) {
line2C return (vxHashPart3(Ch, Hash) ^ ((vxHashPart3(Ch, Hash) & 0xF0000000) >> 23));
line2D }
line2E
line2F constexpr uint32_t vxHashPart1(char Ch, uint32_t Hash) {
line30 return (vxHashPart2(Ch, Hash) & 0x0FFFFFFF);
line31 }
line32
line33 constexpr uint32_t vxHash(const char *Str) {
line34 return (*Str) ? (vxHashPart1(*Str, vxHash(Str + 1))) : (0);
line35 }
line36
line37 // Compile-time generator for list of indexes (0, 1, 2, ...)
line38 template<uint32_t...>
line39 struct vxIndexList {
line3A };
line3B template<typename IndexList, uint32_t Right>
line3C struct vxAppend;
line3D template<uint32_t... Left, uint32_t Right>
line3E struct vxAppend<vxIndexList<Left...>, Right> {
line3F typedef vxIndexList<Left..., Right> Result;
line40 };
line41 template<uint32_t N>
line42 struct vxIndexes {
line43 typedef typename vxAppend<typename vxIndexes<N - 1>::Result, N - 1>::Result Result;
line44 };
line45 template<>
line46 struct vxIndexes<0> {
line47 typedef vxIndexList<> Result;
line48 };
line49
line4A template<uint8_t XorKey, uint8_t BitShiftKey, typename IndexList>
line4B struct vxEncStr;
line4C
line4D template<uint8_t XorKey, uint8_t BitShiftKey, uint32_t... Idx>
line4E struct vxEncStr<XorKey, BitShiftKey, vxIndexList<Idx...> > {
line4F uint8_t Value[sizeof...(Idx) + 1]; // Buffer for a string
line50 // 这里若不设置alinline则最大加密50字节
line51
line52 constexpr force_inline uint8_t vxEncCh(const char Ch, uint32_t Idx_) const {
line53 // do XOR
line54 return (uint8_t)((((Ch & 0xFF) ^ ((XorKey + Idx_) & 0xFF)) >> BitShiftKey) |
line55 (((Ch & 0xFF) ^ ((XorKey + Idx_) & 0xFF)) << (CHAR_BIT - BitShiftKey)));
line56 }
line57
line58
line59 // Compile-time constructor 有的编译器数组初始化使用'{',有的使用'('
line5A constexpr force_inline vxEncStr(const char *const Str) : Value{vxEncCh(Str[Idx], Idx)...} {
line5B static_assert(BitShiftKey < 8 && BitShiftKey >= 0, "Invalild BitShiftKey");
line5C static_assert(XorKey < 0x100 && XorKey >= 0, "Invalild XorKey");
line5D }
line5E
line5F // Run-time decryption
line60 char *decrypt() {
line61 for (uint32_t Idx_ = 0; Idx_ < sizeof...(Idx); Idx_++) {
line62 // do XoR XorKey + Idx_ may exceed 255
line63 Value[Idx_] = (uint8_t)((((Value[Idx_] & 0xFF) << BitShiftKey) |
line64 ((Value[Idx_] & 0xFF) >> (CHAR_BIT - BitShiftKey))) ^ ((XorKey + Idx_) & 0xFF));
line65 }
line66 Value[sizeof...(Idx)] = '\0';
line67 return (char*)Value;
line68 }
line69 };
line6A
line6B // Compile-time hashing macro, hash values changes using the first pseudorandom number in sequence
line6C #define vxHASH(Str) (uint32_t)(vxConstantify<vxHash(Str)>::VALUE ^ vxConstantify<vxRandom(1)>::VALUE)
line6D // Compile-time string encryption macro
line6E #define vxStrEnc_(Size, Str) (vxEncStr<vxRANDOM(0, 0xFF), vxRANDOM(0, CHAR_BIT - 1), \
line6F vxIndexes<Size - 1>::Result>(Str).decrypt())
line70 #define vxStrEnc(Str) vxStrEnc_(sizeof(Str), Str)
line71 #define vxStrEncDbl(Str) vxStrEnc_(sizeof(Str), vxStrEnc_(sizeof(Str), Str))
line72 #define vxStrEncTri(Str) vxStrEnc_(sizeof(Str), vxStrEnc_(sizeof(Str), vxStrEnc_(sizeof(Str), Str)))
说明如下:
- vxRandom为模板元函数,用于在编译期生成随机数,(模拟rand实现),每次调用该函数以获取不同随机数
- vxHASH为模板元函数,用于在编译期对静态字符串生成散列值
- vxStrEnc为模板元函数,用于在编译期对静态字符串实施加密,这样在编译生成的二进制中只会存在经过加密的16进制序列,该序列在运行期在vxStrEnc调用点使用decrypt解密。注意decrypt需要在运行期执行因此不能是模板元函数
- vxStrEncDbl和vxStrEncTri是进行多次加密,增强复杂度
- 模板元函数对于静态字符串或静态数组的处理,是通过模板迭代成单个元素,这一过程较为复杂,上述代码借助vxIndexList生成一个静态索引数组来实现迭代字符串元素
综上,模板元编程是将运行时的某些操作放到编译期来做,提高了程序运行效率,也会略微增加生成二进制大小;而上述代码的加密极大增强了分析复杂度,结合OLLVM效果更好
调试检测
常用调试器进程名检测/二进制匹配
- 对于C调试器采用进程检测
如果当前进程被调试则必然存在调试器进程,因此可以通过进程名方式检测,存在下面是常见调试器对应的进程名及二进制中特征字符串
IDA -> android_server / android_server_pie -> “IDA Android 32-bit remote”
GDB -> gdb / gdbserver -> “GNU gdbserver”
LLDB -> lldbserver
- Adbwifi类APP进程检测
adbwifi是在Root权限下,使用wifi进行Java/C层调试的工具,因此若存在这种进程有可能设备已经Root或正在进行调试,我收集了一些常用包名用于检测这类调试器:
com.gikir.gikdbg com.soynerdito.adbnetworkenabler
adb.wifi.woaiwhz.wifiadbandroid com.twiceyuan.devmode
chendx.wifiadb.main com.vn.tooa.adbwireless
cn.com.wxtech.adbwireless com.wmendez.adbovertcp
com.adam.adbWifi com.youzi.adbwifi
com.adb com.zhoupeng.adbwireless
com.androidfree.adbwifi fr.ydelouis.yrelessadb
com.eboy.adbwireless hcursor.adbcontrol
com.fly.wifiadb info.nakajimadevnakajima.adboverlanswitcher
com.ilovn.app.wifi_adb jps.android.adbtcp
com.liam_w.networkadb me.meowo.adb
com.palmcrust.yawadb moe.haruue.wadb
com.rair.adbwifi net.wiagames.adbon
com.rockolabs.adbkonnect rfo.mougino.waf
com.ryosoftware.adbw ru.bartwell.easyremoteadb
com.scopionstudio.adbwifi vn.android.adbwireless
za.co.henry.hsu.adbwirelessbyhenry
二进制匹配和进程名检测技术总结:
优点:二进制文件比对这种检测方式可以弥补进程名检测的不足。
缺点:始终是最简单的调试检测方式,也最容易被绕过
常用调试器端口检测
移动端采用远程调试器的调试方式很常见,其结构是远程调试服务器+本地调试器,通过调试协议进行通信,其中远程调试器server端安装在移动设备上,完成如下断点等所有实际操作,而本地调试器位于主机上,接受用户指令转化成调试协议数据发送给服务端。作为服务器就要在设备端绑定端口,因此可以检测某些默认端口号来检测调试器的存在,如:IDA默认调试端=23946。(同理还有注入服务器、Hook服务器)
本地端口开放状态可以通过读取/proc/net/tcp和/proc/net/udp解析。Android基于linux系统,存在特殊的unix文件句柄用于socket通信,Android Studio使用lldb调试采用这种方式调试,可以在/proc/net/unix发现踪迹。
由于采用默认端口的服务端很少,因此对处于ESTABLISHED状态的服务端,这里提供协议测试方式分辨调试服务器(对于已连接状态的服务端无法检测)(该方式由于与服务器进行通信,有一定可能使本地服务器崩溃),目的如下:
- 检测调试器
在本地调试器连接到该远程调试服务器之前,通过发送特定bit请求,观察服务器响应,如果响应匹配调试器的响应bit,则判定为调试器
- 反调试
若判定为调试器,则不进行释放socket句柄操作,这样对于只接受单连接的服务器,本地调试器无法再连接该服务器,也就无法继续调试,如GDB的gdbserver、IDA的android_server。某些特殊调试器甚至存在允许关闭自身的指令,因此可以通过发送特定bit请求使服务器关闭。这种方式对于接受多链接的服务器无效。
一次典型的gdb协议通信如下:
Send Rreceive
+ => qSupported:multiprocess+;swbreak+;hwbreak+;
QStartNoAckMode => +$OK
调试器端口检测技术总结:
优点:端口协议测试这种检测方式,对于尚未发起调试攻击过程有一定阻碍作用
缺点:无法对已经建立的调试过程产生影响
硬件可调试性检测
这部分是对设备是否开启调试功能及APP自身是否开启调试功能的这类环境变量进行检测,包括如下字段:
init.svc.adbd adbd是否运行
persis.sys.usb.config usb是否连接
sys.usb.config 。。。。
sys.usb.state 。。。。
ro.debuggable 设备是否可调试
development_settings_enabled 是否开启开发者模式
FLAG_DEBUGGABLE app是否可调试
isDebuggerConnected JDWP调试器是否连接(dalvik/art)
其中ro.debuggable需要设备拥有Root权限才可修改,开启后所有App均处于可调试状态,无论App本身的设置,FLAG_DEBUGGABLE则是App自身对Java层可调试性的配置,Release版会自动设为False
进程调试状态检测
C层调试器在连接后,调用到系统调用ptrace,这个过程会留下一些痕迹,最明显的是/proc/pid/status中的TracerPid字段,该处存放调试器的进程ID。该标记可由本进程、父进程、子进程读取。同理/proc/pid/stat第二个字段会在本进程在调试器中暂停时被系统置为T/t,但由于本进程已经暂停,该标记只能由父进程或子进程读取。类似的还有/proc/pid/wchan的ptrace_stop标志。
上面这些仅仅是针对子进程,然而linux支持只对某个线程做调试,这样就可以绕过上面的检测,因此这里需要遍历所有子进程并检测标志位,由于调试器附加进程后会写进程和所有子线程的标志位,因此无需对/proc/pid/做单独检测,目前计划采用fork子进程方式监测如下位置
/proc/[pid]/task/[tid]/status TracerPid: [pid]
/proc/[pid]/task/[tid]/stat T/t
/proc/[pid]/task/[tid]/wchan ptrace_stop
另一个检测点是ptrace的PTRACE_TRACEME,对于App进程该语句会让zygote附加调试自身,由于只能被一个调试器调试,因此其他调试器无法继续调试本进程。而如果该语句执行失败说明有调试器优先zygote附加本进程。
作为辅助检测点,检测isDebuggerConnected及 jni层的dalvik/art实现dvmDbgIsDebuggerConnected/Dbg::IsDebuggerActive
断点检测
在进行调试时攻击者经常下断点以便拦截到代码执行或数据抓取,因此对通过断点检测到调试器存在,断点类型如下
- C层软件断点
软件断点是通过修改特定指令所在的内存数据为特殊指令,使进程在执行到该处时因为断点指令或异常而中断在调试器中。因此可以通过比对代码与文件的数据比较检测到断点的存在(也可能是hook或其他情况)。为了提高速度,先暂存文件数据的哈希值,每次检测将该哈希与内存哈希进行比较,若不同再检测是否存在断点。下面是常见的软件断点机器码模式(参照GDB/LLDB/IDA等调试器的实现)
ARCH SWBP(little-endian)
Arm 01 00 9f ef
arm-eabi f0 01 f0 e7
thumb 01 de
thumb2 f0 f7 00 a0
mips 0d 00 05 00
arm64 00 00 20 d4 (aarch64)
x86/x64 cc
这里特殊的一点是软件断点可以通过异常实现,因此理论上写入无效指令触发异常即可,无需一定匹配上述模式
- C层硬件(观察)断点
硬件断点是处理器提供的用于调试的一组寄存器功能,通过设置寄存器实现在内存处读、写、执行时中断到调试器。因此通过检测特定寄存器的数据,可以检测是否被调试。(暂不实现)
文件系统监控检测
调试器在附加进程后会读取该进程/proc/pid下的一些数据,其中一些行为可明显区分出调试器行为
Gdbserver 读取/proc/pid/mem 读写内存
Android_server 读取/proc/pid/maps 获取模块信息
反调试
Java虚拟机反调试
原理:Android Java层调试采用JDWP调试协议,而该协议实现于libart.so/libdvm.so中,远程调试过程是一个网络通信过程,从源码分析可以发现JdwpState是一个函数指针数组,部分结构如下:
void *accept; // 服务端接收远端调试连接
void *establish; // 建立调试器服务端
void *shutdown; // 结束进程
void *processIncoming; // 处理远端发来的调试消息
调试过程:establish->accept->processIncoming->processIncoming->…->shutdown;每当用户下断点时,主机上客户端(jdb)会将该请求发到移动端的调试器服务端(libdvm),调用processIncoming处理该消息。因此只要将processIncoming指向自定义回调即可检测到JDWP调试过程,而使其指向shutdown则会在下次调试事件时退出进程。JDWP调试分为adb和socket两种方式,因此有2套JdwpState对象,分别处理通过socket直接进行JDWP调试和通过adb桥进行JDWP调试的通信逻辑
实现:dalvik虚拟机实现在libdvm.so中,而art虚拟机实现在libart.so中;libdvm.so导出dvmJdwpAndroidAdbTransport/dvmJdwpAndroidSocketTransport函数,可得到getState函数指针,通过调用getState()得到JdwpState结构;libart.so则导出art::JDWP::JdwpAdbState/art::JDWP::JdwpSocketState虚表指针,也是JdwpState类似结构。利用该方式实现Java调试检测:
- 将processIncoming设置为自定义回调,可以检测到启动以后adbd尝试连接app jdwp端口的行为,adbd只有打开开发者模式以后才会存在。调试器一旦连接则会经过processIncoming,但经过processIncoming的连接不一定是调试器行为,也可能是adbd的通信
- 由于Android7.0增加了系统保护机制,使得dlopen对某些敏感动态库操作失败,因此采用Hook read函数,检测jdwp协议握手字段JDWP-Handshake。若sosafe模块加载晚于jdwp调试过程建立,该法无效
Java虚拟机反调试技术总结:
优点:利用Android虚拟机自身原理进行反调试,隐蔽性和实用性较强,使调试器拒绝服务
缺点:Android7.0开启了系统动态库保护机制,无法使用获取到私有函数,无法利用这种方式
JNI层反调试
由于JNI调试器(gdbserver/android_server/…)最终通过内核ptrace与应用程序交互,在检测到调试器直接退出进程是明智的选择,这里检测到调试器后的执行延时退出,使用内联汇编的exit函数或异常机制实现退出,避免被很容易的检测到。后期加入擦除回溯栈之类的保护措施,防止被调试器跟踪到关键代码
挂钩检测
常用HOOK工具包名检测
一般挂钩是通过注入和Hook框架工具,取得目标进程控制权,然后通过inline hook/got hook等方式进行挂钩。因此收集Xposed/Substrate等工具安装信息作为最简单的检测方式,包名分别为de.robv.android.xposed.installer com.saurik.substrate。这里先通过java层获取package那么,如果失败则通过执行pm list package命令从native层直接获取所有安装包名,避免被拦截到
系统文件改动检测
Xposed/Substrate这类工具通过修改系统文件的方式,尽可能将自身在最早的时机加载:
- Substrate通过替换liblog.so,将自身加载时机提前到app_process运行前,liblog.so加载前的时机
- Xposed通过替换app_process,将自身加载时机提前到app_process执行时
通过检测替换后文件特征,确定系统文件是否被修改过,即可得知系统是否安装Hook框架;由于app_process可能有/system/bin/app_process /system/bin/app_process32 /system/bin/app_process64共存的情况,所以取当前进程/proc/self/exe指向的app_process文件为准
进程模块检测
Xposed/Frida这类工具通过在zygote中加载模块实现某些功能,由于zygote是所有App进程的孵化器,因此zygote注入的模块自然的被App进程继承。通过检测zygote进程和App进程的加载模块(/proc/self/maps),可以检测是否被挂钩和注入,已知Android平台挂钩/注入工具模块名如下
ProbeDroid libProbeDroid.so
Frida frida-agent.so frida-gadget.so
Cydia Substrate libsubstrate.so libDalvikLoader.so libAndroidCydia.so libAndroidLoader.so *.cy.so
Xposed XposedBridge.jar
Dynamorio libdynamorio.so
由于某些是开源项目,模块名可以人为修改,因此在模块名检测的基础上增加对二进制模块文件的匹配,其中.cy.so是用于substrate的自定义注入模块
Java层调用栈检测
在Hook框架生效时,如果当前线程恰好在Hook框架调用范围内,会在线程回溯栈产生Hook框架自身的函数和类名:
Xposed de.robv.android.xposed.XposedBridge
Cydia Substarte com.saurik.substrate.MS$2.invoked
经过分析,这种方式并不可取,因为要求当前线程已经处于被hook框架调用的代码中,检测率极低且结果也不可靠
Java类成员函数属性校验
由于常见的对于Java函数的挂钩方式是修改Java函数为native函数实现,因此通过检测各个Java类成员函数是否为Native属性判断是否存在Java层挂钩。在检测前提前下发一个包含所有非native函数(包括所有系统和app的类成员函数)的数据表,在检测时刻比对当前所有已加载类的native函数存在该表中的情况,即为存在挂钩。(由于App可随时加载dex,且每次检测只能获取到当前已load的class,因此下发非native表比native函数表可靠)。这种方式目前只实现了Dalvik部分,Art由于各版本私有函数存在差异,比较复杂。
这种方式利用的是JDWP通信接口,以Dalvik为例,dvmDbgGetClassList函数可以获取当前所有已加载类,dvmDbgOutputAllMethods函数可以获取到所有类成员函数信息,通过这两个函数就可以遍历所有已加载类成员函数。
加载模块内存校验
由于sosafe模块加载时机可能晚于挂钩时机,因此需要比对文件和内存模块的区别,判断是否存在Jni层挂钩。通常hook框架使用inline hook/got hook,因此会修改elf代码段和got表,这样就会产生和文件不一致的情况。通过检测文件和内存哈希监测是否存在Jni层hook,即使是Java层Hook,仍然会在Native层有Hook行为,因此这种方式可以检测Java/Jni层Hook。具体操作如下:
- 启动时初始化模块表,记录elf文件的.text段哈希
- 在Hook检测时,获取内存elf的.text段哈希,若不同则判断是否为几种情况之一:软件断点 /Inline Hook/文件修改,若发现inline hook需要判断跳转点是否位于厂商模块/系统自带模块/百度安全组件,不是则记录
- 在Hook检测时,枚举所有got表元素,检查指针所属模块,不是厂商模块/系统自带模块/百度安全组件则记录
注入检测
常用注入工具进程名检测/二进制匹配
检测常见注入工具的服务器进程,如Frida-server adbiserver
检测环境变量注入
检测LD_LIBRARY_PATH LD_PRELOAD是否存在不合法路径
加载模块名检测/二进制匹配
与挂钩检测相同
常用注入工具端口检测
Frida-server常用27042端口通信
ROOT检测
常用ROOT工具路径包名检测
- Root工具:利用漏洞下载su到本地,如360一键root
- 刷机工具:通过刷入Root过的系统镜像实现Root,如刷机大师
- 权限管理工具:在已获得Root权限的机器上管理Root权限
- 其他工具:需要Root权限执行的app,如hook框架
常见su文件路径检测
设备Root后,一定在本地存在su文件以便提权,因此可以通过检测su存在判断设备是否root。Su文件路径选择常见路径+环境变量$PATH,su文件名选择收集到的su文件名。后期可能加入检测su文件是否可执行的逻辑,而由于是否成功获取root权限取决于权限管理器,因此可能给用户弹窗或获取失败的情况
常见路径 常见su
/sbin/ su
/vendor/bin/ us
/system/sbin/ .su
/system/bin/ .suv
/system/xbin/ .suo
/system/usr/ .uv
/system/bin/.ext/ au
/system/bin/failsafe/ k.sud
/system/sd/sbin/ ku.sud
/system/sd/xbin/ .rgs
/system/sd/bin/ .tmpsu
/system/usr/we-need-root/ daemonsu
/system/sd/ kinguser_su"
/system/ cm_su
/data/local/
/data/local/bin/
/data/local/xbin/
/data/local/sbin/
常见系统目录权限修改检测
未Root的手机某些文件夹是拥有r--或---权限的,如默认情况/data文件夹不可读,而在root后这些文件夹属性会发生变化,利用这个特性可检测是否root,默认只读目录:
/data /vendor/bin /system/bin /etc
/ /sys /system/xbin /proc
/system /sbin /system/sbin /dev
虚拟机检测
检测设备特征
某些模拟器会在以下系统属性中存在特征,比如nox,goldfish, ttVM….
ro.build.description
ro.hardware
ro.product.brand
ro.product.device
ro.product.manufacturer
...
有些模拟器采用默认手机号:
15555215554
15555215556
15555215558
...
有些模拟器采用固定hardwareid
000000000000000
e21833235b6eef10
012345678912345
...
有些模拟器是基于qemu/virtualBox,因此系统目录存在特殊文件:
/dev/socket/genyd
/dev/socket/baseband_genyd
/dev/socket/qemud
/dev/qemu_pipe
...