Android新攻防技术研究与应用

2018-12-12  本文已影响0人  超哥__

通用软件保护手段

本文着重介绍①和③,其他方式网上资料较多这里不再赘述。对于②提供如下参考资料:
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引入的新概念,用于将一些简单函数执行流程(分支/循环)放在编译期完成,以提高运行期性能甚至完成一些特殊功能。在展示一个完整的模板元编程实现字符串加密函数前,这里先提供一些准备知识:

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)))

说明如下:

调试检测

常用调试器进程名检测/二进制匹配

IDA  -> android_server / android_server_pie -> “IDA Android 32-bit remote”
GDB  -> gdb / gdbserver -> “GNU gdbserver”
LLDB -> lldbserver
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状态的服务端,这里提供协议测试方式分辨调试服务器(对于已连接状态的服务端无法检测)(该方式由于与服务器进行通信,有一定可能使本地服务器崩溃),目的如下:

一次典型的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

断点检测

  在进行调试时攻击者经常下断点以便拦截到代码执行或数据抓取,因此对通过断点检测到调试器存在,断点类型如下

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

  这里特殊的一点是软件断点可以通过异常实现,因此理论上写入无效指令触发异常即可,无需一定匹配上述模式

文件系统监控检测

  调试器在附加进程后会读取该进程/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调试检测:

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这类工具通过修改系统文件的方式,尽可能将自身在最早的时机加载:

进程模块检测

  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工具路径包名检测

常见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      
...
上一篇 下一篇

猜你喜欢

热点阅读