Android安全机制

最后更新于:2022-04-02 05:05:17

### **概述** Android应用程序是运行在一个沙箱中。这个沙箱是基于Linux内核提供的用户ID(UID)和用户组ID(GID)来实现的。Android应用程序在安装的过程中,安装服务PackageManagerService会为它们分配一个唯一的UID和GID,以及根据应用程序所申请的权限,赋予其它的GID。有了这些UID和GID之后,应用程序就只能限访问特定的文件,一般就是只能访问自己创建的文件。此外,Android应用程序在调用敏感的API时,系统检查它在安装的时候会没有申请相应的权限。如果没有申请的话,那么访问也会被拒绝。对于有root权限的应用程序,则不受上述沙箱限制。此外,有root权限的应用程序,还可以通过Linux的ptrace注入到其它应用程序进程,以及系统进程,进行各种函数调用拦截。 本系列主要讲代码加壳、注入和拦截技术的,包括: 1. SO注入。也就是从一个进程向另外一个进程注入一个SO文件,通过该注入的SO文件就可以实现函数拦截功能。 2. SO加壳。加壳的目的自然就是加大别人对自己的C/C++代码进行静态逆向难度了,这个技术的关键是要实现一个能纯内存操作的Linker了。也就是说,解密后的SO文件内容是保存在一个内存缓冲区的,然后再针对该内存缓冲区进行解析和链接,最终形成一段可执行的代码。这个过程不会产生任何文件供别人做静态分析。 3. C/C++函数GOT拦截。通过修改SO的GOT项来实现函数拦截。这个技术的特点是简单和稳定,但是不足之处于它是针对函数的调用方进行拦截的,而不是针对函数本身的实现来进行拦截的。这样当我们想对某一个函数进行拦截的时候,就必须要检查进程内所有的模块,然后对调用了目标函数的模块的相关GOT 项进行修改。此外,如果某一个模块是通过动态SO加载技术(dlopen、dlsym)来调用目标函数的话,GOT拦截就失效了,因为动态SO加载技术不会产生GOT项。 4. C/C++函数INLINE拦截。这种方法是直接对目标函数的前面几条指令进行修改,用来实现拦截技术。INLINE拦截没有上述GOT拦截的缺点,但是它的实现会复杂很多。由于绝大部分Android设备都是基于ARM架构,因此这里只讨论ARM架构的C/C++函数INLINE拦截。ARMl架构主要分为ARM和THUMB两种指令集,也就是在Android设备上运行的C/C++函数分为ARM和THUMB两种类型。对于ARM指令集的函数,对它们进行拦截至少需要修改头8个字节;对于THUMB指令集,对它们进行拦截至少需要修改头12个字节。无论ARM指令还是THUMB指令函数,我们要修改的头8个字节或者12个字节都很容易碰到跳转或者PC相对寻址指令,这样就需要对指令进行重定位。这个重定位工作相当于繁重和麻烦,得实现一个ARM和THUMB指令解析库才行。不像X86的函数INLINE拦截,只需要函数的头5个字节即可,而且这5个字节几乎都是堆栈相关的操作,不会涉及到跳转或者PC相对寻址指令。 5. DEX注入。在SO注入的基础上,要对目标进程进行DEX注入是相当简单的,通过DexClassLoader即可实现。 6. DEX加壳。DEX加壳与SO加壳一样,都要求在解密之后,能够进行纯内存操作,中间不要产生任何和DEX或者ODEX文件,否则的话,就会给别提供静态分析的机会,这样就失去了加壳的目的。 7. Java函数拦截。与C/C++函数拦截相对,Java函数拦截要优雅得多,因为所有的Java函数都是通过虚拟机来执行的。Dalvik虚拟机执行的函数分为Java和Native两种,它们都是使用Method结构体来描述。当一个Method结构体描述的是一个Java函数时,它有一个成员变量就指向该Java函数的方法区。而当一个Method结构体描述的是一个Native函数,它有一个成员变量指向该Native函数的地址。因此,主要我们能将一个用来描述Java函数的Method结构体修改为一个指向Native函数的Method结构体,就可以骗过Dalvik虚拟机来执行我们所指定的Native函数,从而实现拦截。 以上7个技术点涵盖了Android安全的攻与防基础。在这些基础上不仅可以保护我们自己的代码,还可以对别人的代码进行攻击。 * Android安全模型 * SO注入技术 * SO加壳技术 * C/C++函数拦截技术 * DEX注入技术 * DEX加壳技术 * Java函数拦截技术 #### **Android安全模型** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/3f0d2b143607df6f07356a8b039f7263_394x217.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/78b0f90cd60a9caa3b6dcba231347c41_619x372.png) **用户** * 系统中可以存在多个用户,每一个用户都具有一个UID * 用户按组划分形成用户组,每一个用户组都具有一个GID * 一个用户可以属于多个用户组 **文件** * 每一个文件都具有三种权限 Read、Write、Execute * 文件权限按用户属性分为三组 Owner、Group、Other ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/faced38fa41365815c90cc76f50fc0f1_827x544.png) **进程** * UID -- setuid * GID -- setgid * Supplementary GIDS – setgroups * Capabilities -- capset ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/68f5dbba5a1568e18ecd86b3b3ca1f90_795x537.png) * 系统中的第一个进程Init的UID是root * 子进程的UID默认与父进程相同,但可以通过setuid进行修改 * 子进程被fork之后exec了一个设置了SUID位的bin文件,那么子进程的UID变为该bin文件的Ower UID ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/c076c4303308e2baaa9f7f1199dfcb79_727x181.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/3460d11085d6fde58a32b7bd323b88f9_612x275.png) **每一个APK在安装的时候,PMS都会给它分配一个唯一的UID和GID** * 如果两个APK具有相同的签名,那么可以通过android:sharedUserId申请分配相同的UID和GID * 如果一个APK具有平台签名,那么可以通过android:sharedUserId=“android.uid.system”获得System UID > **备注** > 通过Master Key漏洞获得System UID > http://drops.wooyun.org/papers/219 > http://safe.baidu.com/2013-10/android-masterkey-9695860.html > http://safe.baidu.com/2013-11/masterkey-9950697.html > 打开文件/data/local.prop,设置以下属性: > `ro.kernel.qemu=1` > 即可使得adb具有root权限 **每一个APK都可以通过申请若干个Permission** * 有些Permission需要具有平台签名才可以申请,如INSTALL_PACKAGES **每一个Permission都对应于一个Supplementary GID,因此,给APK分配Permission即为APK分配Supplementary GID** http://developer.android.com/reference/android/Manifest.permission.html **APK进程是由UID为root的Zygote进程fork出来的,fork之后**: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/0023bc63d94db2a1bcc838123965dc26_800x482.png) http://man7.org/linux/man-pages/man2/capset.2.html **PMS记录有每一个APK所申请的Permission,当APK调用敏感API时,相应的模块就会通过PMS会验证调用APK是否申请有相应的Permission** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/834798c84859e17f646c1c85b2ea4b7d_774x305.png) **突破沙箱** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/5af0468ae7e75ad25a8d716735f0db75_729x458.png) * **突破沙箱:创建其它APK也能访问的文件** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/c9ce31d3ded93d312900c02d50b28a3b_1036x165.png) * **突破沙箱:Binder IPC** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2de0c8df1c411f3bafa2f1fdd6b76ad9_781x450.png) * **突破沙箱:Content Provider** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/d2ac0f1a190b62a13dd2ec6be4b3fce7_761x337.png) * **突破沙箱:黑客技术** * SO注入 - C/C++函数拦截 - DEX注入 - Java函数拦截 - …… #### **SO注入技术** **ptrace** ~~~ long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); ~~~ http://man7.org/linux/man-pages/man2/ptrace.2.html ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/09d5249854ddae50433a3362cc896f9e_457x361.png) * Step 1: PTRACE_ATTACH到目标进程,并且让目标进程发生PTRACE_SYSCALL时停止 * Step 2: PTRACE_GETREGS保存目标进程的上下文 * Step 3: PTRACE_SETREGS改写目标进程的PC寄存器,使得它指向函数mmap的地址 * Step 4: PTRACE_CONT让目标进程恢复执行,这时候将会执行函数mmap * Step 5: PTRACE_GETREGS获得目标进程的R0寄存器值,即为函数mmap的返回值,指向在目标进程地址空间分配的一块内存 * Step 6: PTRACE_POKETEXT往在目标进程分配的地址写入以下一段SHELL CODE ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/be4badc25d1590f051a2f94d806ef01f_813x444.png) * Step 7: PTRACE_SETREGS改写目标进程的PC寄存器,使得它指向上述SHELL CODE的起始地址_inject_start_s * Step 8: PTRACE_DETTACH目标进程,目标进程恢复执行后,就会执行注入的 * Step 9: 注入的SHELL CODE在目标进程加载一个SO,并且找到这个SO的指定入口函数,进行调用 #### **SO加壳技术** **系统中的SO文件由一个叫Linker的加载器负责加载,即调用dlopen函数进行加载**: ~~~ void *dlopen(const char *filename, int flag); ~~~ 如果能将第一个参数改为一个内存地址,就可以实现从内存加载SO的功能,进而可以对该内存进行加密处理 **dlopen** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/e1d5d4523c41d7808cedcd50281e4d6d_491x287.png) **find_library** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/158ec1e42211b5b99910794c52716654_591x350.png) **load_library** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/ff95d6318446480d7ce76a0cd5060057_648x507.png) **Read Elf32_Ehdr** Elf32_Ehdr header[1]; read(fd.fd, (void*)header, sizeof(header)) http://man7.org/linux/man-pages/man5/elf.5.html **ReadElf32_Phdr** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/06c02819506c1be5cbd1328b90d13c44_804x572.png) **Reserve Enough Memory** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/3599579b0cfc31c2b003940b0afbda76_711x553.png) **Load Segments** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/f0045c85294ad668e391d76572e640cc_716x666.png) **What data we need?** Elf32_Ehdr Elf32_Phdr Segments **How to fill above data? ** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/dee65aff7e714622cfa6ddfa8ecfa17c_549x171.jpg) **struct elfinfo** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2136f001c8f856f3546c373ee924d3a7_332x177.png) **Open file and create elfinfo** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/cf4dba1ac2298f57323fbde85ef492f3_512x220.png) **Read Elf32_Ehdr** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/12ec737911227705d55e23f66fdcc08e_609x145.png) **Read Elf32_Phdr** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/0246198dbe2a1449c4a5fc1e636c4b9e_762x183.png) **Read segments** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/f5664493e5011e07da07970c21e24766_663x344.png) #### **C/C++函数拦截技术** * Got Hook * VTable Hook * Inline Hook **Got Hook** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/7a29bba9054d7e599dd567bd24741dd3_888x543.png) * **Step 1: Find the address of eglSwapBuffers** ~~~ void * handle = dlopen(“/system/lib/libEGL.so”, RTLD_NOW) void* addr = dlsym(handle, “eglSwapBuffers”); ~~~ * **Step 2: Find the .got section ** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/629714d0b4e95b381ff7713270b0bba5_740x278.png) * **Step 3: Find the address of eglSwapBuffers in .got section** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/00d47b31d69f77abc5c6f37024cb6654_648x176.png) * **Step 4: Replace it** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/cf46eaf7b5690340277f5ea60b6f217a_797x251.png) **VTable Hook** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2e28e9cdcfc42ebf07635fecb79b1594_896x562.png) * **Step 1: Find the address of Surface::unlockAndPost ** ~~~ void * handle = dlopen(“/system/lib/libgui.so”, RTLD_NOW) void* addr = dlsym(handle,“_ZN7android7Surface13unlockAndPostEv”); ~~~ * **Step 2: Find the .data.rel.ro section** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/8ef459c80d2856362c353693fbbda594_759x265.png) * **Step 3: Find the address of Surface::unlockAndPost in .data.rel.ro section ** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/5c65328ddf355cabefb5f1f3ed667b55_764x175.png)、 * **Step 4: Replace it** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/cf46eaf7b5690340277f5ea60b6f217a_797x251.png) **Inline Hook** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/809c5fc44df0208e984e1ea848549a0d_648x534.png) * **Problem** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/8cd16754795b884afcca7451f41f2956_497x338.png)移动的指令可能包含: **普通指令** * PC相对寻址指令 * 跳转指令 **对于PC相对寻址和跳转指令**: * 需要进行重定位 **因此,实现Inline Hook要求**: * 动态的指令解析 * 动态的指令重定位 #### **DEX注入技术** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/aec648f67b895c0f98d30e85a844b481_448x414.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/47d375ffab1c3cabcb7e2ca74033e988_1190x712.png) #### **DEX加壳技术** * 通过DexClassLoader可以动态地加载DEX文件,但是它在加载DEX文件的过程会生成一个ODEX文件,给别人提供了静态逆向的可能 * 通过分析DexClassLoader的实现可以知道,它是通过DexFile来实现动态加载DEX文件的 * 进一步分析DexFile的实现,发现它提供了两个隐藏接口来实现加载内存DEX文件 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/b92912095ecae222f452bd3f81ce806b_891x269.png) ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/efd4bf1705eb13d63cdccfb5bb26b238_764x706.png) * 但是,DexFile从Android 4.0开始才支持加载内存DEX文件,如何支持Android 4.0以下的版本呢? * 通过分析DexFile加载内存DEX文件的实现可以发现,里面用到的关键函数都可以从libdex.a和libdvm.so获得 * 于是,可以模仿Android 4.0,实现DexFile加载内存DEX文件的功能 * 辅助数据结构和函数 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/05c4442c3376dbe3783ca2f73b5a1bf8_737x407.png) * **Step 1: custome_load_class_from_memory** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/41f7c8b0697298433d1a7002d9b0d4f7_1015x656.png) * **Step 2: open_dex_from_memory** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/e15585307e794cb70d4a207cc2c76669_757x621.png) * **Step 3: raw_dex_file_open_array** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/9a151f0b49a17c3787de87c74a8c9db7_773x309.png) * **Step 4: prepare_dex_in_memory** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/3be6a7c817c52387dd6ec9dbeccc7fed_681x275.png) * **Step 5: rewrite_dex ** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/51047ef47a51d0e13403c3e7c234c7fa_751x654.png) * **Step 6: dex_file_open_partial** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/3d95f6be35110f409f567f32d92364e8_743x585.png) * **Step 7: allocate_aux_structures** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/6af58febdd752660c13c9296a02a5dae_672x691.png) * **Step 8: add_to_dex_file_table** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/b3080bd253a04fdbaa9331dec3acb140_711x469.png) * 上述过程用到的关键函数均可从libdex.a和libdvm.so获得: * dexSwapAndVeriry * dexCreateClassLookup * dexFileParse * dvmAllocRegion * dvmAllocAtomicCache * dvmHashTableLock * dvmHashTableLookup * dvmHashTableUnlock #### **Java函数拦截技术** * 在Dalvik虚拟机中,无论是Java函数,还是Native函数,都是通过Method结构体来描述的 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/52380063cc25e7063abb85a917feca2d_702x392.png) * Dalvik虚拟机通过dvmIsNativeMethod判断一个函数是Java函数还是Native函数 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/476c92786b73391740068e107e55712e_506x55.png) * Dalvik虚拟调用一个函数之前,首先判断它是Java函数还是Native函数 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/9b04b1597e470c5276aa6ff94e7a6f94_702x496.png) * Java函数拦截技术原理分析 * 对于Java函数,Davik虚拟机使用解释器来执行 * 对于Native函数, Davik虚拟机找到它的函数指针nativeFunc,进行直接调用 * 如果我们能把一个Java函数修改为Native函数,并且将nativeFunc指针设置为自定义的函数,那么就可以实现拦截了 * 拦截完成之后,根据情况决定是否需要调用原来的Java函数,即可完成整个拦截过程 * libdvm导出了两个函数dvmDecodeIndirectRef和dvmSlotToMethod,如果我们知道一个Java函数在它所属的Class里面的位置Slot,那么就可以通过它们获得该Java函数在Dalvik虚拟内部所对应的Method结构体: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/812850146e131832da2197f1c68525b4_1078x140.png) * 得到一个Java函数在Dalvik虚拟内部所对应的Method结构体之后,就可以将它设置为Native函数: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/ed432dc43769c8cf4b57b49e83cfe3a1_531x111.png) **如何获得一个Java函数所属的Class,以及它在该Class的位置Slot呢?** **假设我们知道:** * Java函数的名称—methodName * Java函数的原型—prototype * Java函数的类名称—className * 用来加载该Java类的ClassLoader--classLoader **Step 1: 获得Class对象** ~~~ Class clazz = classLoader.loadClass(className); ~~~ **Step 2: 获得Method对象** ~~~ Method method = clazz.getDeclaredMethod(methodName, prototype); ~~~ **Step 3: 获得clazz的Slot域描述** ~~~ Field field = clazz.getDeclaredField(“Slot”); ~~~ **Step 4: 获得method的slot** ~~~ int slot= field.getInt(method); ~~~ **Step 5: 将clazz和slot通过JNI传递到C/C++层,调用dvmDecodeIndirectRef和dvmSlotToMethod** ~~~ ClassObject* declared_classs = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), clazz); Method* method = dvmSlotToMethod(declared_class, slot); ~~~
';