Android新攻防技术研究与应用

通用软件保护手段

  • 源码层级保护:在编码过程中实现的攻防手段
    • ① 增加分析复杂度:多线程、虚函数、模板、静态STL库、回调等
    • ② 常规攻防能力:AntiHook/AntiDebug/AntiInject/RootDetect/EmulatorDetect/...
    • 混淆技术:
      • ③ 代码混淆:如OLLVM
      • ④ 常量混淆:主要是字符串混淆
  • ⑤ 二进制层级保护:在生成软件后,对二进制进行处理进行加固的手段

本文着重介绍①和③,其他方式网上资料较多这里不再赘述。对于②提供如下参考资料:
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。具体操作如下:

  1. 启动时初始化模块表,记录elf文件的.text段哈希
  2. 在Hook检测时,获取内存elf的.text段哈希,若不同则判断是否为几种情况之一:软件断点 /Inline Hook/文件修改,若发现inline hook需要判断跳转点是否位于厂商模块/系统自带模块/百度安全组件,不是则记录
  3. 在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      
...
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342